# PyTribe: classes to communicate with EyeTribe eye trackers # # author: Edwin Dalmaijer # email: edwin.dalmaijer@psy.ox.ac.uk # # version 4 (21-Jun-2016) import os import copy import json import time import socket import codecs from threading import Lock, Thread from multiprocessing import Event, Process, Queue from py3compat import * # # # # # # EYETRIBE CLASS # The original EyeTribe class from earlier versions of PyTribe. class EyeTribe: """class for eye tracking and data collection using an EyeTribe tracker """ def __init__(self, logfilename='default', host='localhost', port=6555): """Initializes an EyeTribe instance keyword arguments logfilename -- string indicating the log file name, including a full path to it's location and an extension (default = 'default.txt') """ # initialize data collectors self._logfile = codecs.open('%s.tsv' % (logfilename), 'w', u'utf-8') self._separator = u'\t' self._log_header() self._queue = Queue() # initialize connection self._connection = connection(host=host, port=port) self._tracker = tracker(self._connection) self._heartbeat = heartbeat(self._connection) # create a new Lock self._lock = Lock() # initialize heartbeat thread self._beating = True self._heartbeatinterval = self._tracker.get_heartbeatinterval() / 1000.0 self._hbthread = Thread(target=self._heartbeater, args=[self._heartbeatinterval]) self._hbthread.daemon = True self._hbthread.name = 'heartbeater' # initialize sample streamer self._streaming = True self._samplefreq = self._tracker.get_framerate() self._intsampletime = 1.0 / self._samplefreq self._clockdiff = None self._newestframe = self._tracker.get_frame() self._ssthread = Thread(target=self._stream_samples, args=[self._queue]) self._ssthread.daemon = True self._ssthread.name = 'samplestreamer' # initialize data processer self._processing = True self._logdata = False self._currentsample = copy.deepcopy(self._newestframe) self._dpthread = Thread(target=self._process_samples, args=[self._queue]) self._dpthread.daemon = True self._dpthread.name = 'dataprocessor' # start all threads self._hbthread.start() self._ssthread.start() self._dpthread.start() # initialize calibration self.calibration = calibration(self._connection) def start_recording(self): """Starts data recording """ # set self._logdata to True, so the data processing thread starts # writing samples to the log file if not self._logdata: self._logdata = True self.log_message("start_recording") def stop_recording(self): """Stops data recording """ # consolidate the data file on the hard drive # internal buffer to RAM self._logfile.flush() # RAM file cache to disk os.fsync(self._logfile.fileno()) # set self._logdata to False, so the data processing thread does not # write samples to the log file if self._logdata: self.log_message("stop_recording") self._logdata = False def log_message(self, message): """Logs a message to the logfile, time locked to the most recent sample """ # Get the current time. t = time.time() # Make a string in the specific format that the EyeTribe uses: # yyyy-mm-dd HH:MM:SS.000 ts = '%s.%d' % (time.strftime('%Y-%m-%d %H:%M:%S'), round(t % 1, 3)*1000) # Correct the time to EyeTribe time if self._clockdiff != None: t = int(t + self._clockdiff) else: t = '' # assemble line line = self._separator.join(map(str,[u'MSG', ts, t, safe_decode(message)])) # write message self._logfile.write(line + u'\n') # to internal buffer def sample(self): """Returns the most recent point of regard (=gaze location on screen) coordinates (smoothed signal) arguments None returns gaze -- a (x,y) tuple indicating the point of regard """ if self._newestframe == None: return None, None else: return (self._newestframe['avgx'],self._newestframe['avgy']) def pupil_size(self): """Returns the most recent pupil size sample (an average of the size of both pupils) arguments None returns pupsize -- a float indicating the pupil size (in arbitrary units) """ if self._currentsample == None: return None else: return self._newestframe['psize'] def close(self): """Stops all data streaming, and closes both the connection to the tracker and the logfile """ # if we are currently recording, stop doing so if self._logdata: self.stop_recording() # signal all threads to halt self._beating = False self._streaming = False self._processing = False # close the log file self._logfile.close() # close the connection self._connection.close() def _wait_while_calibrating(self): """Waits until the tracker is not in the calibration state """ while self._tracker.get_iscalibrating(): pass return True def _heartbeater(self, heartbeatinterval): """Continuously sends heartbeats to the tracker, to let it know the connection is still alive (it seems to think we could die any moment now, and is very keen on reassurance of our good health; almost like my grandparents...) arguments heartbeatinterval -- float indicating the heartbeatinterval in seconds; note that this is different from the value that the EyeTribe tracker reports: that value is in milliseconds and should be recalculated to seconds here! """ # keep beating until it is signalled that we should stop while self._beating: # do not bother the tracker when it is calibrating #self._wait_while_calibrating() # wait for the Threading Lock to be released, then lock it self._lock.acquire(True) # send heartbeat self._heartbeat.beat() # release the Threading Lock self._lock.release() # wait for a bit time.sleep(heartbeatinterval) def _stream_samples(self, queue): """Continuously polls the device, and puts all new samples in a Queue instance arguments queue -- a multithreading.Queue instance, to put samples into """ # keep streaming until it is signalled that we should stop while self._streaming: # do not bother the tracker when it is calibrating #self._wait_while_calibrating() # wait for the Threading Lock to be released, then lock it self._lock.acquire(True) # get a new sample sample = self._tracker.get_frame() t1 = time.time() # put the sample in the Queue queue.put(sample) # release the Threading Lock self._lock.release() # Update the newest frame self._newestframe = copy.deepcopy(sample) # Calculate the clock difference self._clockdiff = sample['time'] - t1 # pause for half the intersample time, to avoid an overflow # (but to make sure to not miss any samples) time.sleep(self._intsampletime/2) def _process_samples(self, queue): """Continuously processes samples, updating the most recent sample and writing data to a the log file when self._logdata is set to True arguments queue -- a multithreading.Queue instance, to read samples from """ # keep processing until it is signalled that we should stop while self._processing: # wait for the Threading Lock to be released, then lock it self._lock.acquire(True) # read new item from the queue if not queue.empty(): sample = queue.get() else: sample = None # release the Threading Lock self._lock.release() # update newest sample if sample != None: # check if the new sample is the same as the current sample if not self._currentsample['timestamp'] == sample['timestamp']: # update current sample self._currentsample = copy.deepcopy(sample) # write to file if data logging is on if self._logdata: self._log_sample(sample) def _log_sample(self, sample): """Writes a sample to the log file arguments sample -- a sample dict, as is returned by tracker.get_frame """ # assemble new line line = self._separator.join(map(str,[ sample['timestamp'], sample['time'], sample['fix'], sample['state'], sample['rawx'], sample['rawy'], sample['avgx'], sample['avgy'], sample['psize'], sample['Lrawx'], sample['Lrawy'], sample['Lavgx'], sample['Lavgy'], sample['Lpsize'], sample['Lpupilx'], sample['Lpupily'], sample['Rrawx'], sample['Rrawy'], sample['Ravgx'], sample['Ravgy'], sample['Rpsize'], sample['Rpupilx'], sample['Rpupily'] ])) # write line to log file self._logfile.write(line + '\n') # to internal buffer def _log_header(self): """Logs a header to the data file """ # write a header to the data file header = self._separator.join(['timestamp','time','fix','state', 'rawx','rawy','avgx','avgy','psize', 'Lrawx','Lrawy','Lavgx','Lavgy','Lpsize','Lpupilx','Lpupily', 'Rrawx','Rrawy','Ravgx','Ravgy','Rpsize','Rpupilx','Rpupily' ]) self._logfile.write(header + '\n') # to internal buffer self._logfile.flush() # internal buffer to RAM os.fsync(self._logfile.fileno()) # RAM file cache to disk self._firstlog = False # # # # # # # PARALLEL ClASS # Ugly, but sod it: A global variable for the most recent sample. global _current_sample # Class to communicate with an EyeTribe tracker. The actual communications # and logging actually run in a separate Process. This class just sends # commands to that Process. class ParallelEyeTribe: def __init__(self, logfilename='default'): # Set some standard stuff (hard coded now, but can potentially # be passed to the __init__ method in the future.) host = 'localhost' port = 6555 # We need an Event that signals whether the connection to the # EyeTribe is supposed to be open. self._connection_alive = Event() self._connection_alive.set() # We also need a Queue to send commands through. self._command_queue = Queue() # And we need a Queue to receive commands through. self._to_main_queue = Queue() # Start a parallel process that will take care of all EyeTribe # things. It will provide regular heartbeats to keep the connection # alive, it will record gaze data to a file, and it will keep the # most recent sample updated. This is all done in a separate # Process (rather than in Threads) to so that it can be offloaded # to a different CPU core. This prevents the ongoing experiment # (or whatever you're doing in the main Thread) from interfering # with the processing (and recording) of gaze data. self.eyetribe_process = Process(target=_run_eyetribe_process, \ args=[logfilename, host, port, self._connection_alive, \ self._command_queue]) self.eyetribe_process.name = u'pygaze_eyetribe' self.eyetribe_process.daemon = True self.eyetribe_process.start() def start_recording(self): """Starts data recording """ # Send a command to the EyeTribe Process self._command_queue.put(('start_recording', ())) def stop_recording(self): """Stops data recording """ # Send a command to the EyeTribe Process self._command_queue.put(('stop_recording', ())) def log_message(self, message): """Logs a message to the logfile, time locked to the most recent sample """ # Send a command to the EyeTribe Process self._command_queue.put(('log_message', (message))) def sample(self): """Returns the most recent point of regard (=gaze location on screen) coordinates (smoothed signal) arguments None returns gaze -- a (x,y) tuple indicating the point of regard """ global _current_sample if _current_sample == None: return None, None else: return (_current_sample['avgx'], _current_sample['avgy']) def pupil_size(self): """Returns the most recent pupil size sample (an average of the size of both pupils) arguments None returns pupsize -- a float indicating the pupil size (in arbitrary units) """ global _current_sample if _current_sample == None: return None else: return _current_sample['psize'] def close(self): """Stops all data streaming, and closes both the connection to the tracker and the logfile """ # Send a command to the EyeTribe Process self._command_queue.put(('close', ())) # Function that can run in a parallel process, to keep a connection with the # EyeTribe open, and to log data when appropriate. def _run_eyetribe_process(logfilename, host, port, connection_alive, command_queue): """FOR INTERNAL USE ONLY """ # Ugly, but sod it: A global variable for the most recent sample. global _current_sample # Initialise a new _EyeTribe instance to open the connection to the # EyeTribe. tracker = EyeTribe(logfilename=logfilename, host=host, port=port) # Run until the connection is closed. while connection_alive.is_set(): # Check the incoming Queue. if not command_queue.empty(): # Get the oldest command in the Queue. This is a tuple that # contains a string (the command), and a tuple of values # (what they are depends on the specific command). cmd, value = command_queue.get() # Start recording. if cmd == 'start_recording': tracker.start_recording() # Stop recording. elif cmd == 'stop_recording': tracker.stop_recording() # Log a message. elif cmd == 'log_message': tracker.log_message(value[0]) # Close the connection to the EyeTribe. elif cmd == 'close': tracker.close() # Unset the Event that signals that the connection is # alive. connection_alive.clear() # Update the current sample. _current_sample = copy.deepcopy(tracker._currentsample) # # # # # # SUPPORTING CLASSES class connection: """class for connections with the EyeTribe tracker""" def __init__(self, host='localhost', port=6555): """Initializes the connection with the EyeTribe tracker keyword arguments host -- a string indicating the host IP, NOTE: currently only 'localhost' is supported (default = 'localhost') port -- an integer indicating the port number, NOTE: currently only 6555 is supported (default = 6555) """ # properties self.host = host self.port = port self.resplist = [] self.DEBUG = False # initialize a connection self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.host,self.port)) # Create lock self._request_lock = Lock() def request(self, category, request, values): """Send a message over the connection arguments category -- string indicating the query category request -- string indicating the actual request of the message values -- dict or list containing parameters of the request """ # create a JSON formatted string msg = self.create_json(category, request, values) # send the message over the connection self._request_lock.acquire() self.sock.send(msg) # print request in DEBUG mode if self.DEBUG: print("REQUEST: '%s'" % msg) # give the tracker a wee bit of time to reply time.sleep(0.005) # get new responses success = self.get_response() self._request_lock.release() # return the appropriate response if success: for i in range(len(self.resplist)): # check if the category matches if self.resplist[i]['category'] == category: # if this is a heartbeat, return if self.resplist[i]['category'] == 'heartbeat': return self.resplist.pop(i) # if this is another category, check if the request # matches elif 'request' in self.resplist[i] and \ self.resplist[i]['request'] == request: return self.resplist.pop(i) # on a connection error, get_response returns False and a connection # error should be returned else: return self.parse_json('{"statuscode":901,"values":{"statusmessage":"connection error"}}') def get_response(self): """Asks for a response, and adds these to the list of all received responses (basically a very simple queue) """ # try to get a new response try: response = self.sock.recv(32768) # print reply in DEBUG mode if self.DEBUG: print("REPLY: '%s'" % response) # if it fails, revive the connection and return a connection error except socket.error: print("reviving connection") self.revive() response = '{"statuscode":901,"values":{"statusmessage":"connection error"}}' return False # split the responses (in case multiple came in) response = response.split('\n') # add parsed responses to the internal list for r in response: if r: self.resplist.append(self.parse_json(r)) return True def create_json(self, category, request, values): """Creates a new json message, in the format that is required by the EyeTribe tracker; these messages consist of a categort, a request and a (list of) value(s), which can be thought of as class.method.value (for more info, see: http://dev.theeyetribe.com/api/) arguments category -- query category (string), e.g. 'tracker', 'calibration', or 'heartbeat' request -- the request message (string), e.g. 'get' for the 'tracker' category values -- a dict of parameters and their values, e.g. {"push":True, "version":1} OR: a list of parameters, e.g. ['push','iscalibrated'] OR: None to pass no values at all keyword arguments None returns jsonmsg -- a string in json format, that can be directly sent to the EyeTribe tracker """ # error if the values are anything other than a dict, tuple or list if values is not None and type(values) not in [dict, list, tuple]: raise Exception("values should be dict, tuple or list, not '%s' (values = %s)" % (type(values),values)) # create the json message if request == None: jsondict = {"category":category} elif values == None: jsondict = {"category":category, "request":request} else: jsondict = {"category":category, "request":request, "values":values} return json.dumps(jsondict) def parse_json(self, jsonmsg): """Parses a json message as those that are usually returned by the EyeTribe tracker (for more info, see: http://dev.theeyetribe.com/api/) arguments jsonmsg -- a string in json format keyword arguments None returns msg -- a dict containing the information in the json message; this dict has the following content: { "category": "tracker", "request": "get", "statuscode": 200, "values": { "push":True, "iscalibrated":True } } """ # parse json message parsed = json.loads(jsonmsg) return parsed def revive(self): """Re-establishes a connection """ # close old connection self.close() # initialize a connection self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.host,self.port)) def close(self): """Closes the connection to the EyeTribe tracker """ # close the socket connection self.sock.close() class tracker: """class for SDK Tracker state and information related requests""" def __init__(self, connection): """Initializes a tracker instance arguments connection -- a pytribe.connection instance for the currently attached EyeTribe tracker """ self.connection = connection self.push = True def set_connection(self, connection): """Set a new connection arguments connection -- a pytribe.connection instance for the currently attached EyeTribe tracker """ self.connection = connection def get_push(self): """Returns a Booleam reflecting the state: True for push mode, False for pull mode (Boolean) """ # send the request response = self.connection.request('tracker', 'get', ['push']) # return value or error if response['statuscode'] == 200: return response['values']['push'] else: raise Exception("Error in tracker.get_push: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_heartbeatinterval(self): """Returns the expected heartbeat interval in milliseconds (integer) """ # send the request response = self.connection.request('tracker', 'get', ['heartbeatinterval']) # check if the tracker is in push mode if response['statuscode'] == 200: return response['values']['heartbeatinterval'] else: raise Exception("Error in tracker.get_heartbeatinterval: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_version(self): """Returns the version number (integer) """ # send the request response = self.connection.request('tracker', 'get', ['version']) # return value or error if response['statuscode'] == 200: return response['values']['version'] else: raise Exception("Error in tracker.get_version: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_trackerstate(self): """Returns the state of the physcial tracker (integer): 0: TRACKER_CONNECTED tracker is detected and working 1: TRACKER_NOT_CONNECTED tracker device is not connected 2: TRACKER_CONNECTED_BADFW tracker device is connected, but not working due to bad firmware 3: TRACKER_CONNECTED_NOUSB3 tracker device is connected, but not working due to unsupported USB host 4: TRACKER_CONNECTED_NOSTREAM tracker device is connected, but no stream could be received """ # send the request response = self.connection.request('tracker', 'get', ['trackerstate']) # return value of error if response['statuscode'] == 200: return response['values']['trackerstate'] else: raise Exception("Error in tracker.get_trackerstate: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_framerate(self): """Returns the frame rate that the tracker is running at (integer) """ # send the request response = self.connection.request('tracker', 'get', ['framerate']) # return value or error if response['statuscode'] == 200: return response['values']['framerate'] else: raise Exception("Error in tracker.get_framerate: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_iscalibrated(self): """Indicates whether there is a calibration (Boolean) """ # send the request response = self.connection.request('tracker', 'get', ['iscalibrated']) # return value or error if response['statuscode'] == 200: return response['values']['iscalibrated'] else: raise Exception("Error in tracker.get_iscalibrated: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_iscalibrating(self): """Indicates whether the tracker is in calibration mode (Boolean) """ # send the request response = self.connection.request('tracker', 'get', ['iscalibrating']) # return value or error if response['statuscode'] == 200: return response['values']['iscalibrating'] else: raise Exception("Error in tracker.get_iscalibrating: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_calibresult(self): """Gets the latest valid calibration result returns WITHOUT CALIBRATION: None WITH CALIBRATION: calibresults -- a dict containing the calibration results: { 'result': Boolean indicating whether the calibration was succesful 'deg': float indicating the average error in degrees of visual angle 'Ldeg': float indicating the left eye error in degrees of visual angle 'Rdeg': float indicating the right eye error in degrees of visual angle 'calibpoints': list, containing a dict for each calibration point: {'state': integer indicating the state of the calibration point (0 means no useful data has been obtained and the point should be resampled; 1 means the data is of questionable quality, consider resampling; 2 means the data is ok) 'cpx': x coordinate of the calibration point 'cpy': y coordinate of the calibration point 'mecpx': mean estimated x coordinate of the calibration point 'mecpy': mean estimated y coordinate of the calibration point 'acd': float indicating the accuracy in degrees of visual angle 'Lacd': float indicating the accuracy in degrees of visual angle (left eye) 'Racd': float indicating the accuracy in degrees of visual angle (right eye) 'mepix': mean error in pixels 'Lmepix': mean error in pixels (left eye) 'Rmepix': mean error in pixels (right eye) 'asdp': standard deviation in pixels 'Lasdp': standard deviation in pixels (left eye) 'Rasdp': standard deviation in pixels (right eye) } } """ # send the request response = self.connection.request('tracker', 'get', ['calibresult']) # return value or error if response['statuscode'] != 200: raise Exception("Error in tracker.get_calibresult: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) # return True if this was not the final calibration point if not 'calibpoints' in response['values']: return None # if this was the final calibration point, return the results else: # return calibration dict returndict = { 'result':response['values']['calibresult']['result'], 'deg':response['values']['calibresult']['deg'], 'Rdeg':response['values']['calibresult']['degl'], 'Ldeg':response['values']['calibresult']['degr'], 'calibpoints':[] } for pointdict in response['values']['calibresult']['calibpoints']: returndict['calibpoints'].append({ 'state':pointdict['state'], 'cpx':pointdict['cp']['x'], 'cpy':pointdict['cp']['y'], 'mecpx':pointdict['mecp']['x'], 'mecpy':pointdict['mecp']['y'], 'acd':pointdict['acd']['ad'], 'Lacd':pointdict['acd']['adl'], 'Racd':pointdict['acd']['adr'], 'mepix':pointdict['mepix']['mep'], 'Lmepix':pointdict['mepix']['mepl'], 'Rmepix':pointdict['mepix']['mepr'], 'asdp':pointdict['asdp']['asd'], 'Lasdp':pointdict['asdp']['asdl'], 'Rasdp':pointdict['asdp']['asdr'] }) return returndict def get_frame(self): """Returns the latest frame data (dict) { 'timestamp': string time representation, 'time': integer timestamp in milliseconds, 'fix': Boolean indicating whether there is a fixation, 'state': integer 32bit masked tracker state, 'rawx': integer raw x gaze coordinate in pixels, 'rawy': integer raw y gaze coordinate in pixels, 'avgx': integer smoothed x gaze coordinate in pixels, 'avgx': integer smoothed y gaze coordinate in pixels, 'psize': float average pupil size, 'Lrawx': integer raw x left eye gaze coordinate in pixels, 'Lrawy': integer raw y left eye gaze coordinate in pixels, 'Lavgx': integer smoothed x left eye gaze coordinate in pixels, 'Lavgx': integer smoothed y left eye gaze coordinate in pixels, 'Lpsize': float left eye pupil size, 'Lpupilx': integer raw left eye pupil centre x coordinate, 'Lpupily': integer raw left eye pupil centre y coordinate, 'Rrawx': integer raw x right eye gaze coordinate in pixels, 'Rrawy': integer raw y right eye gaze coordinate in pixels, 'Ravgx': integer smoothed x right eye gaze coordinate in pixels, 'Ravgx': integer smoothed y right eye gaze coordinate in pixels, 'Rpsize': float right eye pupil size, 'Rpupilx': integer raw right eye pupil centre x coordinate, 'Rpupily': integer raw right eye pupil centre y coordinate } """ # send the request response = self.connection.request('tracker', 'get', ['frame']) # raise error if needed if response['statuscode'] != 200: raise Exception("Error in tracker.get_frame: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) # calculate pupil size # if both eyes are available, take the average if response['values']['frame']['lefteye']['psize'] > 0 and \ response['values']['frame']['righteye']['psize'] > 0: psize = (response['values']['frame']['lefteye']['psize'] + \ response['values']['frame']['righteye']['psize']) / 2.0 # if only the right eye is available, then use the right eye elif response['values']['frame']['lefteye']['psize'] == 0 and \ response['values']['frame']['righteye']['psize'] > 0: psize = response['values']['frame']['righteye']['psize'] # if only the left eye is available, then use the left eye elif response['values']['frame']['lefteye']['psize'] > 0 and \ response['values']['frame']['righteye']['psize'] == 0: psize = response['values']['frame']['lefteye']['psize'] # if neither eye is available, then use the EyeTribe's standard # missing value (0.0) else: psize = 0.0 # return the data in a dict return { 'timestamp': response['values']['frame']['timestamp'], 'time': response['values']['frame']['time'], 'fix': response['values']['frame']['fix'], 'state': response['values']['frame']['state'], 'rawx': response['values']['frame']['raw']['x'], 'rawy': response['values']['frame']['raw']['y'], 'avgx': response['values']['frame']['avg']['x'], 'avgy': response['values']['frame']['avg']['y'], 'psize': psize, 'Lrawx': response['values']['frame']['lefteye']['raw']['x'], 'Lrawy': response['values']['frame']['lefteye']['raw']['y'], 'Lavgx': response['values']['frame']['lefteye']['avg']['x'], 'Lavgy': response['values']['frame']['lefteye']['avg']['y'], 'Lpsize': response['values']['frame']['lefteye']['psize'], 'Lpupilx': response['values']['frame']['lefteye']['pcenter']['x'], 'Lpupily': response['values']['frame']['lefteye']['pcenter']['y'], 'Rrawx': response['values']['frame']['righteye']['raw']['x'], 'Rrawy': response['values']['frame']['righteye']['raw']['y'], 'Ravgx': response['values']['frame']['righteye']['avg']['x'], 'Ravgy': response['values']['frame']['righteye']['avg']['y'], 'Rpsize': response['values']['frame']['righteye']['psize'], 'Rpupilx': response['values']['frame']['righteye']['pcenter']['x'], 'Rpupily': response['values']['frame']['righteye']['pcenter']['y'] } def get_screenindex(self): """Returns the screen index number in a multi screen setup (integer) """ # send the request response = self.connection.request('tracker', 'get', ['screenindex']) # return value or error if response['statuscode'] == 200: return response['values']['screenindex'] else: raise Exception("Error in tracker.get_screenindex: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_screenresw(self): """Returns the screen resolution width in pixels (integer) """ # send the request response = self.connection.request('tracker', 'get', ['screenresw']) # return value or error if response['statuscode'] == 200: return response['values']['screenresw'] else: raise Exception("Error in tracker.get_screenresw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_screenresh(self): """Returns the screen resolution height in pixels (integer) """ # send the request response = self.connection.request('tracker', 'get', ['screenresh']) # return value or error if response['statuscode'] == 200: return response['values']['screenresh'] else: raise Exception("Error in tracker.get_screenresh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_screenpsyw(self): """Returns the physical screen width in meters (float) """ # send the request response = self.connection.request('tracker', 'get', ['screenpsyw']) # return value or error if response['statuscode'] == 200: return response['values']['screenpsyw'] else: raise Exception("Error in tracker.get_screenpsyw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def get_screenpsyh(self): """Returns the physical screen height in meters (float) """ # send the request response = self.connection.request('tracker', 'get', ['screenpsyh']) # return value or error if response['statuscode'] == 200: return response['values']['screenpsyh'] else: raise Exception("Error in tracker.get_screenpsyh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_push(self, push=None): """Toggles the push state, or sets the state to the passed value keyword arguments push -- Boolean indicating the state: True for push, False for pull None to toggle current returns state -- Boolean indicating the push state """ # check passed value if push == None: # toggle state self.push = self.push != True elif type(push) == bool: # set state to passed value self.push = push else: # error on anything other than None, True or False raise Exception("tracker.set_push: push keyword argument should be a Boolean or None, not '%s'" % push) # send the request response = self.connection.request('tracker', 'set', {'push':str(self.push).lower()}) # return value or error if response['statuscode'] == 200: return self.push else: raise Exception("Error in tracker.set_push: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_version(self, version): """Set the protocol version arguments version -- integer version number """ # send the request response = self.connection.request('tracker', 'set', {'version':version}) # return value or error if response['statuscode'] == 200: return version else: raise Exception("Error in tracker.set_version: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_screenindex(self, index): """Set the screen index arguments index -- integer value indicating the index number of the screen that is to be used with the tracker """ # send the request response = self.connection.request('tracker', 'set', {'screenindex':index}) # return value or error if response['statuscode'] == 200: return index else: raise Exception("Error in tracker.set_screenindex: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_screenresw(self, width): """Set the screen resolution width arguments width -- integer value indicating the screen resolution width in pixels """ # send the request response = self.connection.request('tracker', 'set', {'screenresw':width}) # return value or error if response['statuscode'] == 200: return width else: raise Exception("Error in tracker.set_screenresw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_screenresh(self, height): """Set the screen resolution height arguments height -- integer value indicating the screen resolution height in pixels """ # send the request response = self.connection.request('tracker', 'set', {'screenresh':height}) # return value or error if response['statuscode'] == 200: return height else: raise Exception("Error in tracker.set_screenresh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_screenpsyw(self, width): """Set the physical width of the screen arguments width -- float value indicating the physical screen width in metres """ # send the request response = self.connection.request('tracker', 'set', {'screenpsyw':width}) # return value or error if response['statuscode'] == 200: return width else: raise Exception("Error in tracker.set_screenpsyw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def set_screenpsyh(self, height): """Set the physical height of the screen arguments width -- float value indicating the physical screen height in metres """ # send the request response = self.connection.request('tracker', 'set', {'screenpsyh':height}) # return value or error if response['statuscode'] == 200: return height else: raise Exception("Error in tracker.set_screenpsyh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) class calibration: """class for calibration related requests""" def __init__(self, connection): """Initializes a calibration instance arguments connection -- a pytribe.connection instance for the currently attached EyeTribe tracker """ self.connection = connection def set_connection(self, connection): """Set a new connection arguments connection -- a pytribe.connection instance for the currently attached EyeTribe tracker """ self.connection = connection def start(self, pointcount=9, max_attempts=5): """Starts the calibration, using the passed number of calibration points keyword arguments pointcount -- integer value indicating the amount of calibration points that should be used, which should be at least 7 (default = 9) max_attempts -- the number of times that calibration should be restarted if starting the calibration fails (default=5) """ for attempt in range(max_attempts): # send the request response = self.connection.request('calibration', 'start', {'pointcount':pointcount}) # return value or error if response['statuscode'] == 200: return self.abort() raise Exception("Error in calibration.start: %s (code %d)" \ % (response['values']['statusmessage'],response['statuscode'])) def pointstart(self, x, y): """Mark the beginning of a new calibration point for the tracker to process arguments x -- integer indicating the x coordinate of the calibration point y -- integer indicating the y coordinate of the calibration point returns success -- Boolean: True on success, False on a failure """ # send the request response = self.connection.request('calibration', 'pointstart', {'x':x,'y':y}) # return value or error if response['statuscode'] == 200: return True else: raise Exception("Error in calibration.pointstart: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def pointend(self): """Mark the end of processing a calibration point returns NORMALLY: success -- Boolean: True on success, False on failure AFTER FINAL POINT: calibresults -- a dict containing the calibration results: { 'result': Boolean indicating whether the calibration was succesful 'deg': float indicating the average error in degrees of visual angle 'Ldeg': float indicating the left eye error in degrees of visual angle 'Rdeg': float indicating the right eye error in degrees of visual angle 'calibpoints': list, containing a dict for each calibration point: {'state': integer indicating the state of the calibration point (0 means no useful data has been obtained and the point should be resampled; 1 means the data is of questionable quality, consider resampling; 2 means the data is ok) 'cpx': x coordinate of the calibration point 'cpy': y coordinate of the calibration point 'mecpx': mean estimated x coordinate of the calibration point 'mecpy': mean estimated y coordinate of the calibration point 'acd': float indicating the accuracy in degrees of visual angle 'Lacd': float indicating the accuracy in degrees of visual angle (left eye) 'Racd': float indicating the accuracy in degrees of visual angle (right eye) 'mepix': mean error in pixels 'Lmepix': mean error in pixels (left eye) 'Rmepix': mean error in pixels (right eye) 'asdp': standard deviation in pixels 'Lasdp': standard deviation in pixels (left eye) 'Rasdp': standard deviation in pixels (right eye) } } """ # send the request response = self.connection.request('calibration', 'pointend', None) # return value or error if response['statuscode'] != 200: raise Exception("Error in calibration.pointend: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) # return True if this was not the final calibration point if not 'calibresult' in response['values']: return True # if this was the final calibration point, return the results else: # return calibration dict returndict = { 'result':response['values']['calibresult']['result'], 'deg':response['values']['calibresult']['deg'], 'Rdeg':response['values']['calibresult']['degl'], 'Ldeg':response['values']['calibresult']['degr'], 'calibpoints':[] } for pointdict in response['values']['calibresult']['calibpoints']: returndict['calibpoints'].append({ 'state':pointdict['state'], 'cpx':pointdict['cp']['x'], 'cpy':pointdict['cp']['y'], 'mecpx':pointdict['mecp']['x'], 'mecpy':pointdict['mecp']['y'], 'acd':pointdict['acd']['ad'], 'Lacd':pointdict['acd']['adl'], 'Racd':pointdict['acd']['adr'], 'mepix':pointdict['mepix']['mep'], 'Lmepix':pointdict['mepix']['mepl'], 'Rmepix':pointdict['mepix']['mepr'], 'asdp':pointdict['asdp']['asd'], 'Lasdp':pointdict['asdp']['asdl'], 'Rasdp':pointdict['asdp']['asdr'] }) return returndict def abort(self): """Cancels the ongoing sequence and reinstates the previous calibration (only if there is one!) returns success -- Boolean: True on success, False on failure """ # send the request response = self.connection.request('calibration', 'abort', None) # return value or error if response['statuscode'] == 200: return True else: raise Exception("Error in calibration.abort: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) def clear(self): """Removes the current calibration from the tracker returns success -- Boolean: True on success, False on failure """ # send the request response = self.connection.request('calibration', 'clear', None) # return value or error if response['statuscode'] == 200: return True else: raise Exception("Error in calibration.clear: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) class heartbeat: """class for signalling heartbeats to the server""" def __init__(self, connection): """Initializes a heartbeat instance (not implemented in the SDK yet) arguments connection -- a pytribe.connection instance for the currently attached EyeTribe tracker """ self.connection = connection def set_connection(self, connection): """Set a new connection arguments connection -- a pytribe.connection instance for the currently attached EyeTribe tracker """ self.connection = connection def beat(self): """Sends a heartbeat to the device """ # send the request response = self.connection.request('heartbeat', None, None) # return value or error if response['statuscode'] == 200: return True else: raise Exception("Error in heartbeat.beat: %s (code %d)" % (response['values']['statusmessage'],response['statuscode'])) # # # # # # DEBUG # if __name__ == "__main__": test = EyeTribe() test.start_recording() time.sleep(10) test.stop_recording() test.close() # # # # #