pax_global_header00006660000000000000000000000064115446664300014523gustar00rootroot0000000000000052 comment=7331b8e06c778bf8f949b6fa9aa6866c865e1773 cedrus-opensource-pyxid-b35bec7/000077500000000000000000000000001154466643000170175ustar00rootroot00000000000000cedrus-opensource-pyxid-b35bec7/.gitignore000066400000000000000000000000771154466643000210130ustar00rootroot00000000000000*.pyc .ropeproject distribute-* pyxid.egg-info *.gz dist build cedrus-opensource-pyxid-b35bec7/COPYING000066400000000000000000000027101154466643000200520ustar00rootroot00000000000000Copyright (c) 2010, Cedrus Corporation All rights reserved. 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 Cedrus Corporation 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. cedrus-opensource-pyxid-b35bec7/README.txt000066400000000000000000000050551154466643000205220ustar00rootroot00000000000000Python library for interfacing with Cedrus XID and StimTracker devices XID (eXperiment Interface Device) devices are used in software such as SuperLab, Presentation, and ePrime 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. The developer using this library must poll the attached device(s) for responses. Here's an example of how to do so: import pyxid # get a list of all attached XID devices devices = pyxid.get_xid_devices() dev = devices[0] # get the first device to use if dev.is_response_device(): dev.reset_base_timer() dev.reset_rt_timer() while True: dev.poll_for_response() if dev.response_queue_size() > 0: response = dev.get_next_response() # do something with the response The response is a python dict with the following keys: pressed: True if the key was pressed, False if it was released key: Response pad key pressed by the subject port: Device port the response was from (typically 0) time: value of the Response Time timer when the key was hit/released StimTracker Support for Cedrus StimTracker devices is now included. On StimTracker devices, there are the following methods: set_pulse_duration() activate_line() clear_line() See the docstring for activate_line() for documentation on how to use it. These methods are not available if the device is a response pad. StimTracker is used in software such as SuperLab, Presentation and ePrime for sending event markers. Timers Each Cedrus XID device has an internal timer a Base Timer and a Response Time Timer. The Base Timer should be reset at the start of an experiment. The Response Time timer should be reset whenever a stimulus is presented. At the time of this library release, there is a known issue with clock drift in XID devices. Our hardware/firmware developer is currently looking into the issue. Given the issue, use of the response timer built into the response pads is optional. If you wish to use the time reported from the response pads, do the following after importing the pyxid library: import pyxid pyxid.use_response_pad_timer = True This will return the time in the 'time' field of the dict returned by XidDevice.get_next_response(), otherwise, the 'time' field will contain 0. Windows Specific Issues Sometimes, windows fails at detecting XID devices. Running detect_xid_devices() a second time should result in finding the devices. cedrus-opensource-pyxid-b35bec7/distribute_setup.py000066400000000000000000000366151154466643000230020ustar00rootroot00000000000000#!python """Bootstrap distribute installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from distribute_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import os import sys import time import fnmatch import tempfile import tarfile from distutils import log try: from site import USER_SITE except ImportError: USER_SITE = None try: import subprocess def _python_cmd(*args): args = (sys.executable,) + args return subprocess.call(args) == 0 except ImportError: # will be used for python 2.3 def _python_cmd(*args): args = (sys.executable,) + args # quoting arguments if windows if sys.platform == 'win32': def quote(arg): if ' ' in arg: return '"%s"' % arg return arg args = [quote(arg) for arg in args] return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 DEFAULT_VERSION = "0.6.14" DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" SETUPTOOLS_FAKED_VERSION = "0.6c11" SETUPTOOLS_PKG_INFO = """\ Metadata-Version: 1.0 Name: setuptools Version: %s Summary: xxxx Home-page: xxx Author: xxx Author-email: xxx License: xxx Description: xxx """ % SETUPTOOLS_FAKED_VERSION def _install(tarball): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # installing log.warn('Installing Distribute') if not _python_cmd('setup.py', 'install'): log.warn('Something went wrong during the installation.') log.warn('See the error message above.') finally: os.chdir(old_wd) def _build_egg(egg, tarball, to_dir): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # building an egg log.warn('Building a Distribute egg in %s', to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) finally: os.chdir(old_wd) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') def _do_download(version, download_base, to_dir, download_delay): egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' % (version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): tarball = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, tarball, to_dir) sys.path.insert(0, egg) import setuptools setuptools.bootstrap_install_from = egg def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15, no_fake=True): # making sure we use the absolute path to_dir = os.path.abspath(to_dir) was_imported = 'pkg_resources' in sys.modules or \ 'setuptools' in sys.modules try: try: import pkg_resources if not hasattr(pkg_resources, '_distribute'): if not no_fake: _fake_setuptools() raise ImportError except ImportError: return _do_download(version, download_base, to_dir, download_delay) try: pkg_resources.require("distribute>="+version) return except pkg_resources.VersionConflict: e = sys.exc_info()[1] if was_imported: sys.stderr.write( "The required version of distribute (>=%s) is not available,\n" "and can't be installed while this script is running. Please\n" "install a more recent version first, using\n" "'easy_install -U distribute'." "\n\n(Currently using %r)\n" % (version, e.args[0])) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return _do_download(version, download_base, to_dir, download_delay) except pkg_resources.DistributionNotFound: return _do_download(version, download_base, to_dir, download_delay) finally: if not no_fake: _create_fake_setuptools_pkg_info(to_dir) def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15): """Download distribute from a specified location and return its filename `version` should be a valid distribute version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ # making sure we use the absolute path to_dir = os.path.abspath(to_dir) try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen tgz_name = "distribute-%s.tar.gz" % version url = download_base + tgz_name saveto = os.path.join(to_dir, tgz_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: log.warn("Downloading %s", url) src = urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = src.read() dst = open(saveto, "wb") dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def _no_sandbox(function): def __no_sandbox(*args, **kw): try: from setuptools.sandbox import DirectorySandbox if not hasattr(DirectorySandbox, '_old'): def violation(*args): pass DirectorySandbox._old = DirectorySandbox._violation DirectorySandbox._violation = violation patched = True else: patched = False except ImportError: patched = False try: return function(*args, **kw) finally: if patched: DirectorySandbox._violation = DirectorySandbox._old del DirectorySandbox._old return __no_sandbox def _patch_file(path, content): """Will backup the file then patch it""" existing_content = open(path).read() if existing_content == content: # already patched log.warn('Already patched.') return False log.warn('Patching...') _rename_path(path) f = open(path, 'w') try: f.write(content) finally: f.close() return True _patch_file = _no_sandbox(_patch_file) def _same_content(path, content): return open(path).read() == content def _rename_path(path): new_name = path + '.OLD.%s' % time.time() log.warn('Renaming %s into %s', path, new_name) os.rename(path, new_name) return new_name def _remove_flat_installation(placeholder): if not os.path.isdir(placeholder): log.warn('Unkown installation at %s', placeholder) return False found = False for file in os.listdir(placeholder): if fnmatch.fnmatch(file, 'setuptools*.egg-info'): found = True break if not found: log.warn('Could not locate setuptools*.egg-info') return log.warn('Removing elements out of the way...') pkg_info = os.path.join(placeholder, file) if os.path.isdir(pkg_info): patched = _patch_egg_dir(pkg_info) else: patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) if not patched: log.warn('%s already patched.', pkg_info) return False # now let's move the files out of the way for element in ('setuptools', 'pkg_resources.py', 'site.py'): element = os.path.join(placeholder, element) if os.path.exists(element): _rename_path(element) else: log.warn('Could not find the %s element of the ' 'Setuptools distribution', element) return True _remove_flat_installation = _no_sandbox(_remove_flat_installation) def _after_install(dist): log.warn('After install bootstrap.') placeholder = dist.get_command_obj('install').install_purelib _create_fake_setuptools_pkg_info(placeholder) def _create_fake_setuptools_pkg_info(placeholder): if not placeholder or not os.path.exists(placeholder): log.warn('Could not find the install location') return pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) setuptools_file = 'setuptools-%s-py%s.egg-info' % \ (SETUPTOOLS_FAKED_VERSION, pyver) pkg_info = os.path.join(placeholder, setuptools_file) if os.path.exists(pkg_info): log.warn('%s already exists', pkg_info) return log.warn('Creating %s', pkg_info) f = open(pkg_info, 'w') try: f.write(SETUPTOOLS_PKG_INFO) finally: f.close() pth_file = os.path.join(placeholder, 'setuptools.pth') log.warn('Creating %s', pth_file) f = open(pth_file, 'w') try: f.write(os.path.join(os.curdir, setuptools_file)) finally: f.close() _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) def _patch_egg_dir(path): # let's check if it's already patched pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') if os.path.exists(pkg_info): if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): log.warn('%s already patched.', pkg_info) return False _rename_path(path) os.mkdir(path) os.mkdir(os.path.join(path, 'EGG-INFO')) pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') f = open(pkg_info, 'w') try: f.write(SETUPTOOLS_PKG_INFO) finally: f.close() return True _patch_egg_dir = _no_sandbox(_patch_egg_dir) def _before_install(): log.warn('Before install bootstrap.') _fake_setuptools() def _under_prefix(location): if 'install' not in sys.argv: return True args = sys.argv[sys.argv.index('install')+1:] for index, arg in enumerate(args): for option in ('--root', '--prefix'): if arg.startswith('%s=' % option): top_dir = arg.split('root=')[-1] return location.startswith(top_dir) elif arg == option: if len(args) > index: top_dir = args[index+1] return location.startswith(top_dir) if arg == '--user' and USER_SITE is not None: return location.startswith(USER_SITE) return True def _fake_setuptools(): log.warn('Scanning installed packages') try: import pkg_resources except ImportError: # we're cool log.warn('Setuptools or Distribute does not seem to be installed.') return ws = pkg_resources.working_set try: setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', replacement=False)) except TypeError: # old distribute API setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) if setuptools_dist is None: log.warn('No setuptools distribution found') return # detecting if it was already faked setuptools_location = setuptools_dist.location log.warn('Setuptools installation detected at %s', setuptools_location) # if --root or --preix was provided, and if # setuptools is not located in them, we don't patch it if not _under_prefix(setuptools_location): log.warn('Not patching, --root or --prefix is installing Distribute' ' in another location') return # let's see if its an egg if not setuptools_location.endswith('.egg'): log.warn('Non-egg installation') res = _remove_flat_installation(setuptools_location) if not res: return else: log.warn('Egg installation') pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') if (os.path.exists(pkg_info) and _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): log.warn('Already patched.') return log.warn('Patching...') # let's create a fake egg replacing setuptools one res = _patch_egg_dir(setuptools_location) if not res: return log.warn('Patched done.') _relaunch() def _relaunch(): log.warn('Relaunching...') # we have to relaunch the process # pip marker to avoid a relaunch bug if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: sys.argv[0] = 'setup.py' args = [sys.executable] + sys.argv sys.exit(subprocess.call(args)) def _extractall(self, path=".", members=None): """Extract all members from the archive to the current working directory and set owner, modification time and permissions on directories afterwards. `path' specifies a different directory to extract to. `members' is optional and must be a subset of the list returned by getmembers(). """ import copy import operator from tarfile import ExtractError directories = [] if members is None: members = self for tarinfo in members: if tarinfo.isdir(): # Extract directories with a safe mode. directories.append(tarinfo) tarinfo = copy.copy(tarinfo) tarinfo.mode = 448 # decimal for oct 0700 self.extract(tarinfo, path) # Reverse sort directories. if sys.version_info < (2, 4): def sorter(dir1, dir2): return cmp(dir1.name, dir2.name) directories.sort(sorter) directories.reverse() else: directories.sort(key=operator.attrgetter('name'), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: dirpath = os.path.join(path, tarinfo.name) try: self.chown(tarinfo, dirpath) self.utime(tarinfo, dirpath) self.chmod(tarinfo, dirpath) except ExtractError: e = sys.exc_info()[1] if self.errorlevel > 1: raise else: self._dbg(1, "tarfile: %s" % e) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" tarball = download_setuptools() _install(tarball) if __name__ == '__main__': main(sys.argv[1:]) cedrus-opensource-pyxid-b35bec7/pyxid/000077500000000000000000000000001154466643000201545ustar00rootroot00000000000000cedrus-opensource-pyxid-b35bec7/pyxid/__init__.py000066400000000000000000000024321154466643000222660ustar00rootroot00000000000000# -*- coding: utf-8 -*- from pyxid_impl import * # Flag for whether or not the response pad timer value should be # returned from XidDevice.get_next_response() # # Set this to true if you want to return the time reported by the # response pad. See the README file for reasons you may not want to # do this. use_response_pad_timer = False def get_xid_devices(): """ Returns a list of all Xid devices connected to your computer. """ devices = [] scanner = XidScanner() for i in range(scanner.device_count()): com = scanner.device_at_index(i) com.open() device = XidDevice(com) devices.append(device) return devices def get_xid_device(device_number): """ returns device at a given index. Raises ValueError if the device at the passed in index doesn't exist. """ scanner = XidScanner() com = scanner.device_at_index(device_number) com.open() return XidDevice(com) def test_event_loop(devices): for d in devices: d.reset_base_timer() d.reset_rt_timer() while True: for d in devices: if d.is_response_device(): d.poll_for_response() if d.response_queue_size() > 0: print d.device_name, d.get_next_response() cedrus-opensource-pyxid-b35bec7/pyxid/constants.py000066400000000000000000000002071154466643000225410ustar00rootroot00000000000000# -*- coding: utf-8 -*- NO_KEY_DETECTED, FOUND_KEY_DOWN, FOUND_KEY_UP = range(3) KEY_RELEASE_BITMASK = 0x10 INVALID_PORT_BITS = 0x0C cedrus-opensource-pyxid-b35bec7/pyxid/internal.py000066400000000000000000000147231154466643000223510ustar00rootroot00000000000000# -*- coding: utf-8 -*- from struct import unpack import time from constants import NO_KEY_DETECTED, FOUND_KEY_DOWN, FOUND_KEY_UP, \ KEY_RELEASE_BITMASK, INVALID_PORT_BITS import pyxid class XidConnection(object): def __init__(self, serial_port, baud_rate=115200): self.serial_port = serial_port self.serial_port.baudrate = baud_rate self.__needs_interbyte_delay = True self.__xid_packet_size = 6 self.__default_read_timeout = 0 self.__bytes_in_buffer = 0 self.__response_buffer = '' self.__last_resp_pressed = False self.__last_resp_port = 0 self.__last_resp_key = 0 self.__last_resp_rt = 0 self.__first_valid_packet = -1 self.__using_stim_tracker = False # the set lines cmd on RB-series and other XID response # devices begins with 'ah'. If, however, a StimTracker is # being used, this will be set to 'mh' instead. 'mh' is to be # used for StimTracker only. It has no effect on response devices. self.__set_lines_cmd = 'ah'+chr(0)+chr(0) self.__line_state = 0 def set_using_stim_tracker(self, using_st=True): if using_st: self.__using_stim_tracker = True self.__set_lines_cmd = 'mh'+chr(0)+chr(0) self.__needs_interbyte_delay = False else: self.__using_stim_tracker = False self.__set_lines_cmd = 'ah'+chr(0)+chr(0) self.__needs_interbyte_delay = True def clear_digital_output_lines(self, lines, leave_remaining_lines=False): if lines not in range(0, 256): raise ValueError('lines must be between 0 and 255') local_lines = ~lines if local_lines < 0: local_lines += 256 if leave_remaining_lines: local_lines = local_lines & self.__line_state if self.__using_stim_tracker: self.__set_lines_cmd = 'mh'+chr(local_lines)+chr(0) else: tmp_lines = ~local_lines if tmp_lines < 0: tmp_lines += 256 self.__set_lines_cmd = 'ah'+chr(tmp_lines)+chr(0) self.write(str(self.__set_lines_cmd)) self.__line_state = local_lines def set_digital_output_lines(self, lines, leave_remaining_lines=False): if lines not in range(0, 256): raise ValueError('lines must be between 0 and 255') if leave_remaining_lines: lines |= self.__line_state if self.__using_stim_tracker: self.__set_lines_cmd = 'mh'+chr(lines)+chr(0) else: lines_tmp = ~lines if lines_tmp < 0: lines_tmp += 256 self.__set_lines_cmd = 'ah'+chr(lines_tmp)+chr(0) self.write(str(self.__set_lines_cmd)) self.__line_state = lines def flush_input(self): self.serial_port.flushInput() def flush_output(self): self.serial_port.flushOutput() def open(self): self.serial_port.open() self.flush_input() self.flush_output() def close(self): self.serial_port.close() def send_xid_command(self, command, bytes_expected=0, timeout=0.1): self.write(command) self.serial_port.timeout = timeout response = self.read(bytes_expected) self.serial_port.timeout = self.__default_read_timeout return response def read(self, bytes_to_read): return self.serial_port.read(bytes_to_read) def write(self, command): bytes_written = 0 if self.__needs_interbyte_delay: for char in command: bytes_written += self.serial_port.write(char) time.sleep(0.001) else: bytes_written = self.serial_port.write(command) return bytes_written def check_for_keypress(self): response = self.read(6) response_found = NO_KEY_DETECTED if len(response) > 0: self.__bytes_in_buffer += len(response) self.__response_buffer += response response_found = self.xid_input_found() return response_found def xid_input_found(self): input_found = NO_KEY_DETECTED if self.__bytes_in_buffer >= 6: last_byte_index = self.__bytes_in_buffer - self.__xid_packet_size i = 0 while i <= last_byte_index: try: (k, params, time) = unpack('> 5) if key == 0: key = 8 self.__last_resp_key = key self.__last_resp_rt = time if self.__last_resp_pressed: input_found = FOUND_KEY_DOWN else: input_found = FOUND_KEY_UP i += 1 return input_found def get_current_response(self): """ reads the current response data from the object and returns it in a dict. Currently 'time' is reported as 0 until clock drift issues are resolved. """ response_time = (self.__last_resp_rt if pyxid.use_response_pad_timer else 0) response = {'time': response_time, 'pressed': self.__last_resp_pressed, 'key': self.__last_resp_key, 'port': self.__last_resp_port} self.remove_current_response() return response def remove_current_response(self): if self.__first_valid_packet != -1: self.__response_buffer = self.__response_buffer[ self.__first_valid_packet + self.__xid_packet_size:] self.__bytes_in_buffer -= self.__xid_packet_size self.__first_valid_packet = -1 cedrus-opensource-pyxid-b35bec7/pyxid/keymaps.py000066400000000000000000000016701154466643000222030ustar00rootroot00000000000000# -*- coding: utf-8 -*- rb_530_keymap = {1: 0, 2: -1, 3: 1, 4: 2, 5: 3, 6: 4, 7: -1, 8: -1} rb_730_keymap = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7} rb_830_keymap = {1: 3, 2: 4, 3: 1, 4: 2, 5: 5, 6: 6, 7: 0, 8: 7} rb_834_keymap = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7} lumina_keymap = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: -1, 7: -1, 8: -1} cedrus-opensource-pyxid-b35bec7/pyxid/pyxid_impl.py000066400000000000000000000332761154466643000227170ustar00rootroot00000000000000# -*- coding: utf-8 -*- from serial_wrapper import SerialPort from serial.serialutil import SerialException from struct import unpack from constants import NO_KEY_DETECTED from internal import XidConnection from keymaps import rb_530_keymap, rb_730_keymap, rb_830_keymap, rb_834_keymap, \ lumina_keymap class XidScanner(object): """ Scan the computer for connected XID devices """ def __init__(self): self.__com_ports = SerialPort.available_ports() self.__xid_cons = [] self.detect_xid_devices() def detect_xid_devices(self): """ For all of the com ports connected to the computer, send an XID command '_c1'. If the device response with '_xid', it is an xid device. """ self.__xid_cons = [] for c in self.__com_ports: device_found = False for b in [115200, 19200, 9600, 57600, 38400]: if device_found: break con = XidConnection(c, b) try: con.open() except SerialException: continue con.flush_input() con.flush_output() returnval = con.send_xid_command("_c1", 5) if returnval.startswith('_xid'): device_found = True self.__xid_cons.append(con) if(returnval != '_xid0'): # set the device into XID mode con.send_xid_command('c10') con.flush_input() con.flush_output() con.close() def device_at_index(self, index): """ Returns the device at the specified index """ if index >= len(self.__xid_cons): raise ValueError("Invalid device index") return self.__xid_cons[index] def device_count(self): """ Number of XID devices connected to the computer """ return len(self.__xid_cons) class BaseDevice(object): def __init__(self, connection, name="Unknown XID Device"): self.con = connection self.device_name = name def reset_rt_timer(self): """ Resets the Reaction Time timer. """ self.con.send_xid_command("e5") def reset_base_timer(self): """ Resets the base timer """ self.con.send_xid_command("e1") def query_base_timer(self): """ gets the value from the device's base timer """ (_, _, time) = unpack('' % self.device_name def __repr__(self): return self.__str__() class StimTracker(BaseDevice): """ Class that encapsulates the StimTracker device. The pulse duration defaults to 100ms. To change this, call StimTracker.set_pulse_duration(duration_in_miliseconds) """ _lines = { 1: 1, 2: 2, 3: 4, 4: 8, 5: 16, 6: 32, 7: 64, 8: 128} def __init__(self, connection, name="StimTracker"): BaseDevice.__init__(self, connection, name) self.con.set_using_stim_tracker(True) self.con.send_xid_command('a10') self.con.clear_digital_output_lines(0xff) self.set_pulse_duration(100) def set_pulse_duration(self, duration): """ Sets the pulse duration for events in miliseconds when activate_line is called """ if duration > 4294967295: raise ValueError('Duration is too long. Please choose a value ' 'less than 4294967296.') big_endian = hex(duration)[2:] if len(big_endian) % 2 != 0: big_endian = '0'+big_endian little_endian = [] for i in range(0, len(big_endian), 2): little_endian.insert(0, big_endian[i:i+2]) for i in range(0, 4-len(little_endian)): little_endian.append('00') command = 'mp' for i in little_endian: command += chr(int(i, 16)) self.con.send_xid_command(command, 0) def activate_line(self, lines=None, bitmask=None, leave_remaining_lines=False): """ Triggers an output line on StimTracker. There are 8 output lines on StimTracker that can be raised in any combination. To raise lines 1 and 7, for example, you pass in the list: activate_line(lines=[1, 7]). To raise a single line, pass in just an integer, or a list with a single element to the lines keyword argument: activate_line(lines=3) or activate_line(lines=[3]) The `lines` argument must either be an Integer, list of Integers, or None. If you'd rather specify a bitmask for setting the lines, you can use the bitmask keyword argument. Bitmask must be a Integer value between 0 and 255 where 0 specifies no lines, and 255 is all lines. For a mapping between lines and their bit values, see the `_lines` class variable. To use this, call the function as so to activate lines 1 and 6: activate_line(bitmask=33) leave_remaining_lines tells the function to only operate on the lines specified. For example, if lines 1 and 8 are active, and you make the following function call: activate_line(lines=4, leave_remaining_lines=True) This will result in lines 1, 4 and 8 being active. If you call activate_line(lines=4) with leave_remaining_lines=False (the default), if lines 1 and 8 were previously active, only line 4 will be active after the call. """ if lines is None and bitmask is None: raise ValueError('Must set one of lines or bitmask') if lines is not None and bitmask is not None: raise ValueError('Can only set one of lines or bitmask') if bitmask is not None: if bitmask not in range(0, 256): raise ValueError('bitmask must be an integer between 0 and 255') if lines is not None: if not isinstance(lines, list): lines = [lines] bitmask = 0 for l in lines: if l < 1 or l > 8: raise ValueError('Line numbers must be between 1 and 8 ' '(inclusive)') bitmask |= self._lines[l] self.con.set_digital_output_lines(bitmask, leave_remaining_lines) def clear_line(self, lines=None, bitmask=None, leave_remaining_lines=False): """ The inverse of activate_line. If a line is active, it deactivates it. This has the same parameters as activate_line() """ if lines is None and bitmask is None: raise ValueError('Must set one of lines or bitmask') if lines is not None and bitmask is not None: raise ValueError('Can only set one of lines or bitmask') if bitmask is not None: if bitmask not in range(0, 256): raise ValueError('bitmask must be an integer between 0 and 255') if lines is not None: if not isinstance(lines, list): lines = [lines] bitmask = 0 for l in lines: if l < 1 or l > 8: raise ValueError('Line numbers must be between 1 and 8 ' '(inclusive)') bitmask |= self._lines[l] self.con.clear_digital_output_lines(bitmask, leave_remaining_lines) def __str__(self): return '' % self.device_name def __repr__(self): return self.__str__() class XidError(Exception): pass class XidDevice(object): """ Class for interfacing with a Cedrus XID device. At the beginning of an experiment, the developer should call: XidDevice.reset_base_timer() Whenever a stimulus is presented, the developer should call: XidDevice.reset_rt_timer() Developers Note: Currently there is a known issue of clock drift in the XID devices. Due to this, the dict returned by XidDevice.get_next_response() returns 0 for the reaction time value. This issue will be resolved in a future release of this library. """ def __init__(self, xid_connection): self.con = xid_connection self._impl = None self.init_device() def __del__(self): self.con.close() del self.con def is_stimtracker(self): return isinstance(self._impl, StimTracker) def is_response_device(self): return isinstance(self._impl, ResponseDevice) def init_device(self): """ Initializes the device with the proper keymaps and name """ try: product_id = int(self._send_command('_d2', 1)) except ValueError: product_id = self._send_command('_d2', 1) if product_id == 0: self._impl = ResponseDevice( self.con, 'Cedrus Lumina LP-400 Response Pad System', lumina_keymap) elif product_id == 1: self._impl = ResponseDevice( self.con, 'Cedrus SV-1 Voice Key', None, 'Voice Response') elif product_id == 2: model_id = int(self._send_command('_d3', 1)) if model_id == 1: self._impl = ResponseDevice( self.con, 'Cedrus RB-530', rb_530_keymap) elif model_id == 2: self._impl = ResponseDevice( self.con, 'Cedrus RB-730', rb_730_keymap) elif model_id == 3: self._impl = ResponseDevice( self.con, 'Cedrus RB-830', rb_830_keymap) elif model_id == 4: self._impl = ResponseDevice( self.con, 'Cedrus RB-834', rb_834_keymap) else: raise XidError('Unknown RB Device') elif product_id == 'S': fw_major = int(self._send_command('_d4', 1)) fw_minor = int(self._send_command('_d5', 1)) if fw_major == 0 and fw_minor < 5: raise XidError('Invalid Firmware Version. You must upgrade ' 'your device to firmware release SC05. ' 'Currently installed version: SC%d%d' % ( fw_major, fw_minor)) self._impl = StimTracker( self.con, 'Cedrus StimTracker') elif product_id == -99: raise XidError('Invalid XID device') def _send_command(self, command, expected_bytes): """ Send an XID command to the device """ response = self.con.send_xid_command(command, expected_bytes) return response def __getattr__(self, attrname): return getattr(self._impl, attrname) def __str__(self): if self._impl is not None: return str(self._impl) else: return 'Uninitialized XID device' def __repr__(self): return self.__str__() cedrus-opensource-pyxid-b35bec7/pyxid/serial_wrapper.py000066400000000000000000000057771154466643000235650ustar00rootroot00000000000000# -*- coding: utf-8 -*- from serial import serial_for_url, Serial from serial.serialutil import SerialException import os, sys class SerialPort(object): def __init__(self, serial_port, baud_rate=115200): if sys.platform == 'darwin': self.impl = MacSerialPort(serial_port, baud_rate) elif sys.platform == 'linux2': self.impl = LinuxSerialPort(serial_port, baud_rate) else: self.impl = GenericSerialPort(serial_port, baud_rate) def __getattr__(self, attr): return getattr(self.impl, attr) @staticmethod def available_ports(): if sys.platform == 'darwin': return MacSerialPort.available_ports() elif sys.platform == 'linux2': return LinuxSerialPort.available_ports() else: return GenericSerialPort.available_ports() class LinuxSerialPort(object): """ USB-serial devices on Linux show up as /dev/ttyUSB? pySerial expects /dev/tty? (no USB). """ def __init__(self, serial_port, baud_rate=115200): self.serial_port = serial_port self.serial_port.setBaudrate(baud_rate) def __getattr__(self, attr): return getattr(self.serial_port, attr) @staticmethod def available_ports(): usb_serial_ports = filter( (lambda x: x.startswith('ttyUSB')), os.listdir('/dev')) ports = [] for p in usb_serial_ports: ports.append(serial_for_url('/dev/'+p, do_not_open=True)) return ports class GenericSerialPort(object): def __init__(self, serial_port, baud_rate=115200): self.serial_port = serial_port self.serail_port.setBaudrate(baud_rate) @staticmethod def available_ports(): """ Scans COM1 through COM255 for available serial ports returns a list of available ports """ ports = [] for i in range(256): try: p = Serial(i) p.close() ports.append(p) except SerialException: pass return ports def __getattr__(self, attr): """ forward calls to the internal serial.Serial instance """ return getattr(self.serial_port, attr) class MacSerialPort(object): """ USB-Serial ports on Mac OS show up as /dev/cu.usbserial-* pySerial expects them to be /dev/cuad* """ def __init__(self, serial_port, baud_rate=115200): self.serial_port = serial_port self.serial_port.setBaudrate(baud_rate) @staticmethod def available_ports(): usb_serial_ports = filter( (lambda x : x.startswith('cu.usbserial')), os.listdir('/dev')) ports = [] for p in usb_serial_ports: ports.append(serial_for_url('/dev/'+p, do_not_open=True)) return ports def __getattr__(self, attr): """ forward calls to the internal serial.Serial instance """ return getattr(self.serial_port, attr) cedrus-opensource-pyxid-b35bec7/setup.py000066400000000000000000000020151154466643000205270ustar00rootroot00000000000000from setuptools import setup, find_packages setup( name = "pyxid", version = "1.0", packages = find_packages(), install_requires = ["pyserial>=2.5"], author = "Grant Limberg", author_email = "glimberg@cedrus.com", maintainer = "Cedrus Corporation", maintainer_email = "opensource@cedrus.com", description = ("Pure python library for communicating with Cedrus XID " "and StimTracker devices."), long_description = open('README.txt').read(), license = "BSD", keywords = "cedrus xid XID stimulus response data collection", url = "http://www.github.com/cedrus-opensource/pyxid/", classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Topic :: System :: Hardware", "Programming Language :: Python", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", ] )