Example #1
0
    def __init__(self,
                 display,
                 resolution=settings.DISPSIZE,
                 data_file=settings.LOGFILENAME + ".edf",
                 fg_color=settings.FGC,
                 bg_color=settings.BGC,
                 eventdetection=settings.EVENTDETECTION,
                 saccade_velocity_threshold=35,
                 saccade_acceleration_threshold=9500,
                 blink_threshold=settings.BLINKTHRESH,
                 force_drift_correct=True,
                 pupil_size_mode=settings.EYELINKPUPILSIZEMODE,
                 **args):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # try to import copy docstring (but ignore it if it fails, as we do
        # not need it for actual functioning of the code)
        try:
            copy_docstr(BaseEyeTracker, libeyelink)
        except:
            # we're not even going to show a warning, since the copied
            # docstring is useful for code editors; these load the docs
            # in a non-verbose manner, so warning messages would be lost
            pass

        global _eyelink

        # Make sure that we have a valid data file. The local_data_file may
        # contain a folder. The eyelink_data_file is only a basename, i.e.
        # without folder. The eyelink_data_file must be at most eight characters
        # and end with a `.edf` extension.

        self.local_data_file = data_file
        self.eyelink_data_file = os.path.basename(data_file)
        stem, ext = os.path.splitext(self.eyelink_data_file)
        if len(stem) > 8 or ext.lower() != '.edf':
            raise Exception(
                "The EyeLink cannot handle filenames longer than eight "
                "characters (excluding '.edf' extension).")

        # properties
        self.display = display
        self.fontsize = 18
        self.scr = Screen(disptype=settings.DISPTYPE, mousevisible=False)
        self.kb = Keyboard(keylist=["escape", "q"], timeout=1)
        self.resolution = resolution
        self.recording = False
        self.saccade_velocity_treshold = saccade_velocity_threshold
        self.saccade_acceleration_treshold = saccade_acceleration_threshold
        self.blink_threshold = blink_threshold
        self.eye_used = None
        self.left_eye = 0
        self.right_eye = 1
        self.binocular = 2
        self.pupil_size_mode = pupil_size_mode
        self.prevsample = (-1, -1)
        self.prevps = -1

        # event detection properties
        # degrees; maximal distance from fixation start (if gaze wanders beyond
        # this, fixation has stopped)
        self.fixtresh = 1.5
        # milliseconds; amount of time gaze has to linger within self.fixtresh
        # to be marked as a fixation
        self.fixtimetresh = 100
        # degrees per second; saccade velocity threshold
        self.spdtresh = self.saccade_velocity_treshold
        # degrees per second**2; saccade acceleration threshold
        self.accthresh = self.saccade_acceleration_treshold
        self.set_detection_type(eventdetection)
        # weighted distance, used for determining whether a movement is due to
        # measurement error (1 is ok, higher is more conservative and will
        # result in only larger saccades to be detected)
        self.weightdist = 10
        # distance between participant and screen in cm
        self.screendist = settings.SCREENDIST
        # distance between participant and screen in cm
        self.screensize = settings.SCREENSIZE
        self.pixpercm = (self.resolution[0]/float(self.screensize[0]) + \
         self.resolution[1]/float(self.screensize[1])) / 2.0
        # only initialize eyelink once
        if _eyelink == None:
            try:
                _eyelink = pylink.EyeLink()
            except:
                raise Exception(
                    "Error in libeyelink.libeyelink.__init__(): Failed to "
                    "connect to the tracker!")
        # determine software version of tracker
        self.tracker_software_ver = 0
        self.eyelink_ver = pylink.getEYELINK().getTrackerVersion()
        if self.eyelink_ver == 3:
            tvstr = pylink.getEYELINK().getTrackerVersionString()
            vindex = tvstr.find("EYELINK CL")
            self.tracker_software_ver = int(float(tvstr[(vindex + \
             len("EYELINK CL")):].strip()))
        if self.eyelink_ver == 1:
            self.eyelink_model = 'EyeLink I'
        elif self.eyelink_ver == 2:
            self.eyelink_model = 'EyeLink II'
        elif self.eyelink_ver == 3:
            self.eyelink_model = 'EyeLink 1000'
        else:
            self.eyelink_model = 'EyeLink (model unknown)'
        # Open graphics
        self.eyelink_graphics = EyelinkGraphics(self, _eyelink)
        pylink.openGraphicsEx(self.eyelink_graphics)
        # Optionally force drift correction. For some reason this must be done
        # as (one of) the first things, otherwise a segmentation fault occurs.
        if force_drift_correct:
            try:
                self.send_command('driftcorrect_cr_disable = OFF')
            except:
                print('Failed to force drift correction (EyeLink 1000 only)')
        # Set pupil-size mode
        if self.pupil_size_mode == 'area':
            pylink.getEYELINK().setPupilSizeDiameter(False)
        elif self.pupil_size_mode == 'diameter':
            pylink.getEYELINK().setPupilSizeDiameter(True)
        else:
            raise Exception(
             "pupil_size_mode should be 'area' or 'diameter', not %s" \
             % self.pupil_size_mode)
        pylink.getEYELINK().openDataFile(self.eyelink_data_file)
        pylink.flushGetkeyQueue()
        pylink.getEYELINK().setOfflineMode()
        # notify eyelink of display resolution
        self.send_command("screen_pixel_coords = 0 0 %d %d" % \
         (self.resolution[0], self.resolution[1]))
        # get some configuration stuff
        if self.eyelink_ver >= 2:
            self.send_command("select_parser_configuration 0")
            if self.eyelink_ver == 2:  # turn off scenelink camera stuff
                self.send_command("scene_camera_gazemap = NO")
        # set EDF file contents (this specifies which data is written to the EDF
        # file)
        self.send_command(
            "file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON"
        )
        if self.tracker_software_ver >= 4:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS")
        # set link data (this specifies which data is sent through the link and
        # thus can be used in gaze contingent displays)
        self.send_command(
            "link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON")
        if self.tracker_software_ver >= 4:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS")
        # not quite sure what this means (according to Sebastiaan Mathot, it
        # might be the button that is used to end drift correction?)
        self.send_command("button_function 5 'accept_target_fixation'")

        if not self.connected():
            raise Exception(
                "Error in libeyelink.libeyelink.__init__(): Failed to connect "
                "to the eyetracker!")
Example #2
0
    def __init__(self,
                 display,
                 resolution=DISPSIZE,
                 data_file=LOGFILENAME + ".edf",
                 fg_color=FGC,
                 bg_color=BGC,
                 eventdetection=EVENTDETECTION,
                 saccade_velocity_threshold=35,
                 saccade_acceleration_threshold=9500,
                 force_drift_correct=True,
                 pupil_size_mode=EYELINKPUPILSIZEMODE,
                 **args):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # try to import copy docstring (but ignore it if it fails, as we do
        # not need it for actual functioning of the code)
        try:
            copy_docstr(BaseEyeTracker, libeyelink)
        except:
            # we're not even going to show a warning, since the copied
            # docstring is useful for code editors; these load the docs
            # in a non-verbose manner, so warning messages would be lost
            pass

        global _eyelink

        # Make sure that we have a valid data file. The local_data_file may
        # contain a folder. The eyelink_data_file is only a basename, i.e.
        # without folder. The eyelink_data_file must be at most eight characters
        # and end with a `.edf` extension.
        self.local_data_file = data_file
        self.eyelink_data_file = os.path.basename(data_file)
        stem, ext = os.path.splitext(self.eyelink_data_file)
        if len(stem) > 8 or ext.lower() != '.edf':
            raise Exception(
                "The EyeLink cannot handle filenames longer than eight "
                "characters (excluding '.edf' extension).")

        # properties
        self.display = display
        self.fontsize = 18
        self.scr = Screen(disptype=DISPTYPE, mousevisible=False)
        self.kb = Keyboard(keylist=["escape", "q"], timeout=1)
        self.resolution = resolution
        self.recording = False
        self.saccade_velocity_treshold = saccade_velocity_threshold
        self.saccade_acceleration_treshold = saccade_acceleration_threshold
        self.eye_used = None
        self.left_eye = 0
        self.right_eye = 1
        self.binocular = 2
        self.pupil_size_mode = pupil_size_mode
        self.prevsample = (-1, -1)
        self.prevps = -1

        # event detection properties
        # degrees; maximal distance from fixation start (if gaze wanders beyond
        # this, fixation has stopped)
        self.fixtresh = 1.5
        # milliseconds; amount of time gaze has to linger within self.fixtresh
        # to be marked as a fixation
        self.fixtimetresh = 100
        # degrees per second; saccade velocity threshold
        self.spdtresh = self.saccade_velocity_treshold
        # degrees per second**2; saccade acceleration threshold
        self.accthresh = self.saccade_acceleration_treshold
        self.set_detection_type(eventdetection)
        # weighted distance, used for determining whether a movement is due to
        # measurement error (1 is ok, higher is more conservative and will
        # result in only larger saccades to be detected)
        self.weightdist = 10
        # distance between participant and screen in cm
        self.screendist = SCREENDIST
        # distance between participant and screen in cm
        self.screensize = SCREENSIZE
        self.pixpercm = (self.resolution[0]/float(self.screensize[0]) + \
         self.resolution[1]/float(self.screensize[1])) / 2.0
        # only initialize eyelink once
        if _eyelink == None:
            try:
                _eyelink = pylink.EyeLink()
            except:
                raise Exception(
                    "Error in libeyelink.libeyelink.__init__(): Failed to "
                    "connect to the tracker!")
        # determine software version of tracker
        self.tracker_software_ver = 0
        self.eyelink_ver = pylink.getEYELINK().getTrackerVersion()
        if self.eyelink_ver == 3:
            tvstr = pylink.getEYELINK().getTrackerVersionString()
            vindex = tvstr.find("EYELINK CL")
            self.tracker_software_ver = int(float(tvstr[(vindex + \
             len("EYELINK CL")):].strip()))
        if self.eyelink_ver == 1:
            self.eyelink_model = 'EyeLink I'
        elif self.eyelink_ver == 2:
            self.eyelink_model = 'EyeLink II'
        elif self.eyelink_ver == 3:
            self.eyelink_model = 'EyeLink 1000'
        else:
            self.eyelink_model = 'EyeLink (model unknown)'
        # Open graphics
        self.eyelink_graphics = EyelinkGraphics(self, _eyelink)
        pylink.openGraphicsEx(self.eyelink_graphics)
        # Optionally force drift correction. For some reason this must be done
        # as (one of) the first things, otherwise a segmentation fault occurs.
        if force_drift_correct:
            self.send_command('driftcorrect_cr_disable = OFF')
        # Set pupil-size mode
        if self.pupil_size_mode == 'area':
            pylink.getEYELINK().setPupilSizeDiameter(False)
        elif self.pupil_size_mode == 'diameter':
            pylink.getEYELINK().setPupilSizeDiameter(True)
        else:
            raise Exception(
             "pupil_size_mode should be 'area' or 'diameter', not %s" \
             % self.pupil_size_mode)
        pylink.getEYELINK().openDataFile(self.eyelink_data_file)
        pylink.flushGetkeyQueue()
        pylink.getEYELINK().setOfflineMode()
        # notify eyelink of display resolution
        self.send_command("screen_pixel_coords = 0 0 %d %d" % \
         (self.resolution[0], self.resolution[1]))
        # get some configuration stuff
        if self.eyelink_ver >= 2:
            self.send_command("select_parser_configuration 0")
            if self.eyelink_ver == 2:  # turn off scenelink camera stuff
                self.send_command("scene_camera_gazemap = NO")
        # set EDF file contents (this specifies which data is written to the EDF
        # file)
        self.send_command(
            "file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON"
        )
        if self.tracker_software_ver >= 4:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS")
        # set link data (this specifies which data is sent through the link and
        # thus can be used in gaze contingent displays)
        self.send_command(
            "link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON")
        if self.tracker_software_ver >= 4:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS")
        # not quite sure what this means (according to Sebastiaan Mathot, it
        # might be the button that is used to end drift correction?)
        self.send_command("button_function 5 'accept_target_fixation'")

        if not self.connected():
            raise Exception(
                "Error in libeyelink.libeyelink.__init__(): Failed to connect "
                "to the eyetracker!")
Example #3
0
class libeyelink(BaseEyeTracker):

    MAX_TRY = 100

    def __init__(self,
                 display,
                 resolution=settings.DISPSIZE,
                 data_file=settings.LOGFILENAME + ".edf",
                 fg_color=settings.FGC,
                 bg_color=settings.BGC,
                 eventdetection=settings.EVENTDETECTION,
                 saccade_velocity_threshold=35,
                 saccade_acceleration_threshold=9500,
                 blink_threshold=settings.BLINKTHRESH,
                 force_drift_correct=True,
                 pupil_size_mode=settings.EYELINKPUPILSIZEMODE,
                 **args):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # try to import copy docstring (but ignore it if it fails, as we do
        # not need it for actual functioning of the code)
        try:
            copy_docstr(BaseEyeTracker, libeyelink)
        except:
            # we're not even going to show a warning, since the copied
            # docstring is useful for code editors; these load the docs
            # in a non-verbose manner, so warning messages would be lost
            pass

        global _eyelink

        # Make sure that we have a valid data file. The local_data_file may
        # contain a folder. The eyelink_data_file is only a basename, i.e.
        # without folder. The eyelink_data_file must be at most eight characters
        # and end with a `.edf` extension.

        self.local_data_file = data_file
        self.eyelink_data_file = os.path.basename(data_file)
        stem, ext = os.path.splitext(self.eyelink_data_file)
        if len(stem) > 8 or ext.lower() != '.edf':
            raise Exception(
                "The EyeLink cannot handle filenames longer than eight "
                "characters (excluding '.edf' extension).")

        # properties
        self.display = display
        self.fontsize = 18
        self.scr = Screen(disptype=settings.DISPTYPE, mousevisible=False)
        self.kb = Keyboard(keylist=["escape", "q"], timeout=1)
        self.resolution = resolution
        self.recording = False
        self.saccade_velocity_treshold = saccade_velocity_threshold
        self.saccade_acceleration_treshold = saccade_acceleration_threshold
        self.blink_threshold = blink_threshold
        self.eye_used = None
        self.left_eye = 0
        self.right_eye = 1
        self.binocular = 2
        self.pupil_size_mode = pupil_size_mode
        self.prevsample = (-1, -1)
        self.prevps = -1

        # event detection properties
        # degrees; maximal distance from fixation start (if gaze wanders beyond
        # this, fixation has stopped)
        self.fixtresh = 1.5
        # milliseconds; amount of time gaze has to linger within self.fixtresh
        # to be marked as a fixation
        self.fixtimetresh = 100
        # degrees per second; saccade velocity threshold
        self.spdtresh = self.saccade_velocity_treshold
        # degrees per second**2; saccade acceleration threshold
        self.accthresh = self.saccade_acceleration_treshold
        self.set_detection_type(eventdetection)
        # weighted distance, used for determining whether a movement is due to
        # measurement error (1 is ok, higher is more conservative and will
        # result in only larger saccades to be detected)
        self.weightdist = 10
        # distance between participant and screen in cm
        self.screendist = settings.SCREENDIST
        # distance between participant and screen in cm
        self.screensize = settings.SCREENSIZE
        self.pixpercm = (self.resolution[0]/float(self.screensize[0]) + \
         self.resolution[1]/float(self.screensize[1])) / 2.0
        # only initialize eyelink once
        if _eyelink == None:
            try:
                _eyelink = pylink.EyeLink()
            except:
                raise Exception(
                    "Error in libeyelink.libeyelink.__init__(): Failed to "
                    "connect to the tracker!")
        # determine software version of tracker
        self.tracker_software_ver = 0
        self.eyelink_ver = pylink.getEYELINK().getTrackerVersion()
        if self.eyelink_ver == 3:
            tvstr = pylink.getEYELINK().getTrackerVersionString()
            vindex = tvstr.find("EYELINK CL")
            self.tracker_software_ver = int(float(tvstr[(vindex + \
             len("EYELINK CL")):].strip()))
        if self.eyelink_ver == 1:
            self.eyelink_model = 'EyeLink I'
        elif self.eyelink_ver == 2:
            self.eyelink_model = 'EyeLink II'
        elif self.eyelink_ver == 3:
            self.eyelink_model = 'EyeLink 1000'
        else:
            self.eyelink_model = 'EyeLink (model unknown)'
        # Open graphics
        self.eyelink_graphics = EyelinkGraphics(self, _eyelink)
        pylink.openGraphicsEx(self.eyelink_graphics)
        # Optionally force drift correction. For some reason this must be done
        # as (one of) the first things, otherwise a segmentation fault occurs.
        if force_drift_correct:
            try:
                self.send_command('driftcorrect_cr_disable = OFF')
            except:
                print('Failed to force drift correction (EyeLink 1000 only)')
        # Set pupil-size mode
        if self.pupil_size_mode == 'area':
            pylink.getEYELINK().setPupilSizeDiameter(False)
        elif self.pupil_size_mode == 'diameter':
            pylink.getEYELINK().setPupilSizeDiameter(True)
        else:
            raise Exception(
             "pupil_size_mode should be 'area' or 'diameter', not %s" \
             % self.pupil_size_mode)
        pylink.getEYELINK().openDataFile(self.eyelink_data_file)
        pylink.flushGetkeyQueue()
        pylink.getEYELINK().setOfflineMode()
        # notify eyelink of display resolution
        self.send_command("screen_pixel_coords = 0 0 %d %d" % \
         (self.resolution[0], self.resolution[1]))
        # get some configuration stuff
        if self.eyelink_ver >= 2:
            self.send_command("select_parser_configuration 0")
            if self.eyelink_ver == 2:  # turn off scenelink camera stuff
                self.send_command("scene_camera_gazemap = NO")
        # set EDF file contents (this specifies which data is written to the EDF
        # file)
        self.send_command(
            "file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON"
        )
        if self.tracker_software_ver >= 4:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS")
        # set link data (this specifies which data is sent through the link and
        # thus can be used in gaze contingent displays)
        self.send_command(
            "link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON")
        if self.tracker_software_ver >= 4:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS")
        # not quite sure what this means (according to Sebastiaan Mathot, it
        # might be the button that is used to end drift correction?)
        self.send_command("button_function 5 'accept_target_fixation'")

        if not self.connected():
            raise Exception(
                "Error in libeyelink.libeyelink.__init__(): Failed to connect "
                "to the eyetracker!")

    def send_command(self, cmd):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        pylink.getEYELINK().sendCommand(cmd)

    def log(self, msg):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        pylink.getEYELINK().sendMessage(msg)

    def status_msg(self, msg):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        print('status message: %s' % msg)
        pylink.getEYELINK().sendCommand("record_status_message '%s'" % msg)

    def connected(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        return pylink.getEYELINK().isConnected()

    def calibrate(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        while True:
            if self.recording:
                raise Exception(
                    "Error in libeyelink.libeyelink.calibrate(): Trying to "
                    "calibrate after recording has started!")

            # # # # #
            # EyeLink calibration and validation

            # attempt calibrate; confirm abort when esc pressed
            while True:
                self.eyelink_graphics.esc_pressed = False
                pylink.getEYELINK().doTrackerSetup()
                if not self.eyelink_graphics.esc_pressed:
                    break
                self.confirm_abort_experiment()

            # If we are using the built-in EyeLink event detection, we don't need
            # the RMS calibration routine.
            if self.eventdetection == 'native':
                return

            # # # # #
            # RMS calibration
            while True:
                # present instructions
                self.display.fill()  # clear display
                self.scr.draw_text(text= \
                 "Noise calibration: please look at the dot\n\n(press space to start)",
                 pos=(self.resolution[0]/2, int(self.resolution[1]*0.2)),
                 center=True, fontsize=self.fontsize)
                self.scr.draw_fixation(fixtype='dot')
                self.display.fill(self.scr)
                self.display.show()
                self.scr.clear()  # clear screen again

                # wait for spacepress
                self.kb.get_key(keylist=['space'], timeout=None)

                # start recording
                self.log("PYGAZE RMS CALIBRATION START")
                self.start_recording()

                # show fixation
                self.display.fill()
                self.scr.draw_fixation(fixtype='dot')
                self.display.fill(self.scr)
                self.display.show()
                self.scr.clear()

                # wait for a bit, to allow participant to fixate
                clock.pause(500)

                # get samples
                # samplelist, prefilled with 1 sample to prevent sl[-1] from producing
                # an error; first sample will be ignored for RMS calculation
                sl = [self.sample()]
                t0 = clock.get_time()  # starting time
                while clock.get_time() - t0 < 1000:
                    s = self.sample()  # sample
                    if s != sl[-1] and s != (-1, -1) and s != (0, 0):
                        sl.append(s)

                # stop recording
                self.log("PYGAZE RMS CALIBRATION END")
                self.stop_recording()

                # calculate RMS noise
                Xvar = []
                Yvar = []
                for i in range(2, len(sl)):
                    Xvar.append((sl[i][0] - sl[i - 1][0])**2)
                    Yvar.append((sl[i][1] - sl[i - 1][1])**2)
                if Xvar and Yvar:  # check if properly recorded to avoid risk of division by zero error
                    XRMS = (sum(Xvar) / len(Xvar))**0.5
                    YRMS = (sum(Yvar) / len(Yvar))**0.5
                    self.pxdsttresh = (XRMS, YRMS)

                    # recalculate thresholds (degrees to pixels)
                    self.pxfixtresh = deg2pix(self.screendist, self.fixtresh,
                                              self.pixpercm)
                    self.pxspdtresh = deg2pix(
                        self.screendist, self.spdtresh,
                        self.pixpercm) / 1000.0  # in pixels per millisecons
                    self.pxacctresh = deg2pix(
                        self.screendist, self.accthresh,
                        self.pixpercm) / 1000.0  # in pixels per millisecond**2
                    return
                else:  # if nothing recorded, display message saying so
                    self.display.fill()
                    self.scr.draw_text(text = \
                     "Noise calibration failed.\n\nPress r to retry,\nor press space to return to calibration screen.", \
                     pos=(self.resolution[0]/2, int(self.resolution[1]*0.2)), \
                     center=True, fontsize=self.fontsize)
                    self.display.fill(self.scr)
                    self.display.show()
                    self.scr.clear()
                    # wait for space or r press, if r restart noise calibration, if space return to calibration menu
                    keypressed = self.kb.get_key(keylist=['space', 'r'],
                                                 timeout=None)
                    if keypressed[0] == 'space':
                        break

    def drift_correction(self, pos=None, fix_triggered=False):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.drift_correction(): Trying to "
                "perform drift correction after recording has started!")
        if not self.connected():
            raise Exception(
                "Error in libeyelink.libeyelink.drift_correction(): The "
                "eyelink is not connected!")
        if pos == None:
            pos = self.resolution[0] / 2, self.resolution[1] / 2
        if fix_triggered:
            return self.fix_triggered_drift_correction(pos)
        return self.manual_drift_correction(pos)

    def manual_drift_correction(self, pos):
        """
		Performs a manual, i.e. spacebar-triggered drift correction.

		Arguments:
		pos		--	The positionf or the drift-correction target.

		Returns:
		True if drift correction was successfull, False otherwise.
		"""

        self.draw_drift_correction_target(pos[0], pos[1])
        self.eyelink_graphics.esc_pressed = False
        try:
            # The 0 parameters indicate that the display should not be cleared
            # and we should not be allowed to fall back to the set-up screen.
            error = pylink.getEYELINK().doDriftCorrect(pos[0], pos[1], 0, 0)
        except:
            error = -1
        # A 0 exit code means successful drift correction
        if error == 0:
            return True
        # If escape was pressed, we present the confirm abort screen
        if self.eyelink_graphics.esc_pressed:
            self.confirm_abort_experiment()
        # If 'q' was pressed, we drop back to the calibration screen
        else:
            self.calibrate()
        return False

    def prepare_drift_correction(self, pos):
        """Puts the tracker in drift correction mode"""

        # start collecting samples in drift correction mode
        self.send_command("heuristic_filter = ON")
        self.send_command("drift_correction_targets = %d %d" % pos)
        self.send_command("start_drift_correction data = 0 0 1 0")
        pylink.msecDelay(50)
        # wait for a bit until samples start coming in (again, not sure if this
        # is indeed what's going on)
        if not pylink.getEYELINK().waitForBlockStart(100, 1, 0):
            print(
                "WARNING libeyelink.libeyelink.prepare_drift_correction(): "
                "Failed to perform drift correction (waitForBlockStart error)")

    def fix_triggered_drift_correction(self,
                                       pos=None,
                                       min_samples=30,
                                       max_dev=60,
                                       reset_threshold=10):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.fix_triggered_drift_correction(): "
                "Trying to perform drift correction after recording has started!"
            )

        self.recording = True
        if pos == None:
            pos = self.resolution[0] / 2, self.resolution[1] / 2
        self.prepare_drift_correction(pos)
        self.draw_drift_correction_target(pos[0], pos[1])

        # loop until we have enough samples
        lx = []
        ly = []
        while len(lx) < min_samples:

            # Check whether the EyeLink is put into set-up mode on the EyeLink
            # PC and, if so, jump to the calibration menu.
            if pylink.getEYELINK().getCurrentMode() == pylink.IN_SETUP_MODE:
                self.recording = False
                self.calibrate()
                print(
                    "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                    "'q' pressed")
                return False

            # pressing escape enters the calibration screen
            resp = self.kb.get_key(keylist=["escape", "q"], timeout=1)[0]
            if resp == 'escape':
                self.recording = False
                self.confirm_abort_experiment()
                print(
                    "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                    "'escape' pressed")
                return False
            elif resp == 'q':
                self.recording = False
                self.calibrate()
                print(
                    "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                    "'q' pressed")
                return False
            # collect a sample
            x, y = self.sample()
            if len(lx) == 0 or x != lx[-1] or y != ly[-1]:
                # if present sample deviates too much from previous sample,
                # start from scratch.
                if len(lx) > 0 and (abs(x - lx[-1]) > reset_threshold or \
                 abs(y - ly[-1]) > reset_threshold):
                    lx = []
                    ly = []
                # Collect a sample
                else:
                    lx.append(x)
                    ly.append(y)
            # If we have enough samples to perform a drift correction ...
            if len(lx) == min_samples:
                avg_x = sum(lx) / len(lx)
                avg_y = sum(ly) / len(ly)
                d = ((avg_x - pos[0])**2 + (avg_y - pos[1])**2)**0.5
                # emulate spacebar press on succes
                pylink.getEYELINK().sendKeybutton(32, 0, pylink.KB_PRESS)
                # getCalibrationResult() returns 0 on success and an exception
                # or a non-zero value otherwise
                result = -1
                try:
                    result = pylink.getEYELINK().getCalibrationResult()
                except:
                    lx = []
                    ly = []
                    print(
                        "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                        "try again")
                if result != 0:
                    try:
                        result = pylink.getEYELINK().getCalibrationResult()
                    except:
                        lx = []
                        ly = []
                        print(
                            "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                            "try again")
        # apply drift correction
        pylink.getEYELINK().applyDriftCorrect()
        self.recording = False
        print(
            "libeyelink.libeyelink.fix_triggered_drift_correction(): success")
        return True

    def start_recording(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.recording = True
        i = 0
        while True:
            # params: write samples, write event, send samples, send events
            print(u'starting recording ...')
            error = pylink.getEYELINK().startRecording(1, 1, 1, 1)
            print(u'returned %s' % error)
            if not error:
                break
            if i > self.MAX_TRY:
                raise Exception(
                    "Error in libeyelink.libeyelink.start_recording(): Failed "
                    "to start recording!")
                self.close()
                clock.expend()
            i += 1
            print(
                ("WARNING libeyelink.libeyelink.start_recording(): Failed to "
                 "start recording (attempt %d of %d)") % (i, self.MAX_TRY))
            pylink.msecDelay(100)
        # don't know what this is
        print(u'Start realtime mode ...')
        pylink.msecDelay(100)
        pylink.beginRealTimeMode(100)
        # wait a bit until samples start coming in
        print(u'Wait for block start ...')
        pylink.msecDelay(100)
        if not pylink.getEYELINK().waitForBlockStart(100, 1, 0):
            raise Exception(
                "Error in libeyelink.libeyelink.start_recording(): Failed to "
                "start recording (waitForBlockStart error)!")
        print(u'done ...')

    def stop_recording(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        print(u'stopping recording ...')
        self.recording = False
        pylink.endRealTimeMode()
        pylink.getEYELINK().setOfflineMode()
        pylink.msecDelay(500)
        print(u'done ...')

    def close(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.eyelink_graphics.close()
        if self.recording:
            self.stop_recording()
        # close data file and transfer it to the experimental PC
        print("libeyelink.libeyelink.close(): Closing data file")
        pylink.getEYELINK().closeDataFile()
        pylink.msecDelay(500)
        print("libeyelink.libeyelink.close(): Transferring %s to %s" \
         % (self.eyelink_data_file, self.local_data_file))
        # During data transfer, suppress output
        _out = sys.stdout
        with open(os.devnull, 'w') as fd:
            sys.stdout = fd
            pylink.getEYELINK().receiveDataFile(self.eyelink_data_file,
                                                self.local_data_file)
            sys.stdout = _out
        pylink.msecDelay(500)
        print("libeyelink.libeyelink.close(): Closing eyelink")
        pylink.getEYELINK().close()
        pylink.msecDelay(500)

    def set_eye_used(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.eye_used = pylink.getEYELINK().eyeAvailable()
        if self.eye_used == self.right_eye:
            self.log_var("eye_used", "right")
        elif self.eye_used == self.left_eye or self.eye_used == self.binocular:
            self.log_var("eye_used", "left")
            self.eye_used = self.left_eye
        else:
            print("WARNING libeyelink.libeyelink.set_eye_used(): Failed to "
                  "determine which eye is being recorded")

    def pupil_size(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if not self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.pupil_size(): Recording was "
                "not started before collecting eyelink data!")
        if self.eye_used == None:
            self.set_eye_used()
        # get newest sample
        s = pylink.getEYELINK().getNewestSample()
        # check if sample is new
        if s != None:
            # right eye
            if self.eye_used == self.right_eye and s.isRightSample():
                ps = s.getRightEye().getPupilSize()
            # left eye
            elif self.eye_used == self.left_eye and s.isLeftSample():
                ps = s.getLeftEye().getPupilSize()
            # invalid
            else:
                ps = -1
            # set new pupil size as previous pupil size
            self.prevps = ps
        # if no new sample is available, use old data
        else:
            ps = self.prevps
        return ps

    def sample(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if not self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.sample(): Recording was not "
                "started before collecting eyelink data!")
        if self.eye_used == None:
            self.set_eye_used()
        s = pylink.getEYELINK().getNewestSample()
        if s != None:
            if self.eye_used == self.right_eye and s.isRightSample():
                gaze = s.getRightEye().getGaze()
            elif self.eye_used == self.left_eye and s.isLeftSample():
                gaze = s.getLeftEye().getGaze()
            else:
                gaze = (-1, -1)
            self.prevsample = gaze[:]
        else:
            gaze = self.prevsample[:]
        return gaze

    def set_detection_type(self, eventdetection):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if eventdetection in ['pygaze', 'native']:
            self.eventdetection = eventdetection

        return (self.eventdetection, self.eventdetection, self.eventdetection)

    def _get_eyelink_clock_async(self):
        """
		Retrieve time differenece between tracker timestamps and
		current clock time upheld in the pygaze environment.

		Note that this is not guaranteed to be a static time difference, the
		clocks might run at different speeds. Therefore you should consider
		running this function every time you utilize on this time difference.

		Returns:
		The tracker time minus the clock time
		"""
        return pylink.getEYELINK().trackerTime() - clock.get_time()

    def wait_for_event(self, event):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if not self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.wait_for_event(): Recording "
                "was not started before collecting eyelink data!")

        if self.eye_used == None:
            self.set_eye_used()
        if self.eventdetection == 'native':
            # since the link buffer was not have been polled, old data has
            # accumulated in the buffer -- so ignore events that are old:
            t0 = clock.get_time()  # time of call
            while True:
                d = pylink.getEYELINK().getNextData()
                if d == event:
                    float_data = pylink.getEYELINK().getFloatData()
                    # corresponding clock_time
                    tc = float_data.getTime() - self._get_eyelink_clock_async()
                    if tc > t0:
                        return tc, float_data

        if event == 5:
            outcome = self.wait_for_saccade_start()
        elif event == 6:
            outcome = self.wait_for_saccade_end()
        elif event == 7:
            outcome = self.wait_for_fixation_start()
        elif event == 8:
            outcome = self.wait_for_fixation_end()
        elif event == 3:
            outcome = self.wait_for_blink_start()
        elif event == 4:
            outcome = self.wait_for_blink_end()
        else:
            raise Exception(
                ("Error in libeyelink.libeyelink.wait_for_event: eventcode %s "
                 "is not supported") % event)
        return outcome

    def wait_for_saccade_start(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.STARTSACC)
            return t, d.getStartGaze()

        # # # # #
        # PyGaze method

        else:

            # get starting position (no blinks)
            newpos = self.sample()
            while not self.is_valid_sample(newpos):
                newpos = self.sample()
            # get starting time, position, intersampledistance, and velocity
            t0 = clock.get_time()
            prevpos = newpos[:]
            s = 0
            v0 = 0

            # get samples
            saccadic = False
            while not saccadic:
                # get new sample
                newpos = self.sample()
                t1 = clock.get_time()
                if self.is_valid_sample(newpos) and newpos != prevpos:
                    # check if distance is larger than precision error
                    sx = newpos[0] - prevpos[0]
                    sy = newpos[1] - prevpos[1]
                    # weigthed distance: (sx/tx)**2 + (sy/ty)**2 > 1 means
                    # movement larger than RMS noise
                    if (sx/self.pxdsttresh[0])**2 + (sy/self.pxdsttresh[1])**2 \
                     > self.weightdist:
                        # calculate distance
                        # intersampledistance = speed in pixels/ms
                        s = ((sx)**2 + (sy)**2)**0.5
                        # calculate velocity
                        v1 = s / (t1 - t0)
                        # calculate acceleration
                        a = (v1 - v0) / (t1 - t0
                                         )  # acceleration in pixels/ms**2
                        # check if either velocity or acceleration are above
                        # threshold values
                        if v1 > self.pxspdtresh or a > self.pxacctresh:
                            saccadic = True
                            spos = prevpos[:]
                            stime = clock.get_time()
                        # update previous values
                        t0 = copy.copy(t1)
                        v0 = copy.copy(v1)
                    # udate previous sample
                    prevpos = newpos[:]
            return stime, spos

    def wait_for_saccade_end(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.ENDSACC)
            return t, d.getStartGaze(), d.getEndGaze()

        # # # # #
        # PyGaze method

        else:

            # get starting position (no blinks)
            t0, spos = self.wait_for_saccade_start()
            # get valid sample
            prevpos = self.sample()
            while not self.is_valid_sample(prevpos):
                prevpos = self.sample()
            # get starting time, intersample distance, and velocity
            t1 = clock.get_time()
            # = intersample distance = speed in px/sample
            s = ((prevpos[0] - spos[0])**2 + (prevpos[1] - spos[1])**2)**0.5
            v0 = s / (t1 - t0)
            # run until velocity and acceleration go below threshold
            saccadic = True
            while saccadic:
                # get new sample
                newpos = self.sample()
                t1 = clock.get_time()
                if self.is_valid_sample(newpos) and newpos != prevpos:
                    # calculate distance
                    # = speed in pixels/sample
                    s = ((newpos[0]-prevpos[0])**2 + \
                     (newpos[1]-prevpos[1])**2)**0.5
                    # calculate velocity
                    v1 = s / (t1 - t0)
                    # calculate acceleration
                    # acceleration in pixels/sample**2 (actually is
                    # v1-v0 / t1-t0; but t1-t0 = 1 sample)
                    a = (v1 - v0) / (t1 - t0)
                    # check if velocity and acceleration are below threshold
                    if v1 < self.pxspdtresh and (a > -1*self.pxacctresh and \
                     a < 0):
                        saccadic = False
                        epos = newpos[:]
                        etime = clock.get_time()
                    # update previous values
                    t0 = copy.copy(t1)
                    v0 = copy.copy(v1)
                # udate previous sample
                prevpos = newpos[:]

            return etime, spos, epos

    def wait_for_fixation_start(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.STARTFIX)
            return t, d.getTime(), d.getStartGaze()

        # # # # #
        # PyGaze method

        else:

            # function assumes a 'fixation' has started when gaze position
            # remains reasonably stable for self.fixtimetresh

            # get starting position
            spos = self.sample()
            while not self.is_valid_sample(spos):
                spos = self.sample()

            # get starting time
            t0 = clock.get_time()

            # wait for reasonably stable position
            moving = True
            while moving:
                # get new sample
                npos = self.sample()
                # check if sample is valid
                if self.is_valid_sample(npos):
                    # check if new sample is too far from starting position
                    if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > \
                     self.pxfixtresh**2: # Pythagoras
                        # if not, reset starting position and time
                        spos = copy.copy(npos)
                        t0 = clock.get_time()
                    # if new sample is close to starting sample
                    else:
                        # get timestamp
                        t1 = clock.get_time()
                        # check if fixation time threshold has been surpassed
                        if t1 - t0 >= self.fixtimetresh:
                            # return time and starting position
                            return t1, spos

    def wait_for_fixation_end(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.ENDFIX)
            return t, d.getTime(), d.getStartGaze()

        # # # # #
        # PyGaze method

        else:

            # function assumes that a 'fixation' has ended when a deviation of
            # more than fixtresh from the initial 'fixation' position has been
            # detected

            # get starting time and position
            stime, spos = self.wait_for_fixation_start()

            # loop until fixation has ended
            while True:
                # get new sample
                npos = self.sample()  # get newest sample
                # check if sample is valid
                if self.is_valid_sample(npos):
                    # check if sample deviates to much from starting position
                    if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > \
                     self.pxfixtresh**2: # Pythagoras
                        # break loop if deviation is too high
                        break

            return clock.get_time(), spos

    def wait_for_blink_start(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.STARTBLINK)
            return t, d.getTime()

        # # # # #
        # PyGaze method

        else:

            blinking = False

            # loop until there is a blink
            while not blinking:
                # get newest sample
                gazepos = self.sample()
                # check if it's a valid sample
                if not self.is_valid_sample(gazepos):
                    # get timestamp for possible blink start
                    t0 = clock.get_time()
                    # loop until a blink is determined, or a valid sample occurs
                    while not self.is_valid_sample(self.sample()):
                        # check if time has surpassed 150 ms
                        if clock.get_time() - t0 >= self.blink_threshold:
                            # return timestamp of blink start
                            return t0

    def wait_for_blink_end(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.ENDBLINK)
            return t

        # # # # #
        # PyGaze method

        else:

            blinking = True

            # loop while there is a blink
            while blinking:
                # get newest sample
                gazepos = self.sample()
                # check if it's valid
                if self.is_valid_sample(gazepos):
                    # if it is a valid sample, blinking has stopped
                    blinking = False

            # return timestamp of blink end
            return clock.get_time()

    def set_draw_calibration_target_func(self, func):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.eyelink_graphics.draw_cal_target = func

    # ***
    #
    # Internal functions below
    #
    # ***

    def is_valid_sample(self, gazepos):
        """
		Checks if the sample provided is valid, based on EyeLink specific
		criteria.

		arguments
		gazepos		--	a (x,y) gaze position tuple, as returned by
						self.sample()

		returns
		valid			--	a Boolean: True on a valid sample, False on
						an invalid sample
		"""

        # return False if a sample is invalid
        if gazepos == (-1, -1):
            return False

        # in any other case, the sample is valid
        return True

    def confirm_abort_experiment(self):
        """
		Asks for confirmation before aborting the experiment. Displays a
		confirmation screen, collects the response, and acts accordingly.

		Exceptions:
		Raises a response_error upon confirmation.

		Returns:
		False if no confirmation was given.
		"""

        # Display the confirmation screen
        scr = Screen(disptype=settings.DISPTYPE)
        kb = Keyboard(timeout=5000)
        yc = settings.DISPSIZE[1] / 2
        xc = settings.DISPSIZE[0] / 2
        ld = 40  # Line height
        scr.draw_text(u'Really abort experiment?',
                      pos=(xc, yc - 3 * ld),
                      fontsize=self.fontsize)
        scr.draw_text(u'Press \'Y\' to abort',
                      pos=(xc, yc - 0.5 * ld),
                      fontsize=self.fontsize)
        scr.draw_text(u'Press any other key or wait 5s to go to setup',
                      pos=(xc, yc + 0.5 * ld),
                      fontsize=self.fontsize)
        self.display.fill(scr)
        self.display.show()
        # process the response:
        try:
            key, time = kb.get_key()
        except:
            return False
        # if confirmation, close experiment
        if key == u'y':
            raise Exception(u'The experiment was aborted')
        self.eyelink_graphics.esc_pressed = False
        return False

    def draw_drift_correction_target(self, x, y):
        """
		Draws the drift-correction target.

		arguments

		x		--	The X coordinate
		y		--	The Y coordinate
		"""

        self.scr.clear()
        self.scr.draw_fixation(fixtype='dot',
                               colour=settings.FGC,
                               pos=(x, y),
                               pw=0,
                               diameter=12)
        self.display.fill(self.scr)
        self.display.show()
Example #4
0
class libeyelink(BaseEyeTracker):

    MAX_TRY = 100

    def __init__(self,
                 display,
                 resolution=DISPSIZE,
                 data_file=LOGFILENAME + ".edf",
                 fg_color=FGC,
                 bg_color=BGC,
                 eventdetection=EVENTDETECTION,
                 saccade_velocity_threshold=35,
                 saccade_acceleration_threshold=9500,
                 force_drift_correct=True,
                 pupil_size_mode=EYELINKPUPILSIZEMODE,
                 **args):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # try to import copy docstring (but ignore it if it fails, as we do
        # not need it for actual functioning of the code)
        try:
            copy_docstr(BaseEyeTracker, libeyelink)
        except:
            # we're not even going to show a warning, since the copied
            # docstring is useful for code editors; these load the docs
            # in a non-verbose manner, so warning messages would be lost
            pass

        global _eyelink

        # Make sure that we have a valid data file. The local_data_file may
        # contain a folder. The eyelink_data_file is only a basename, i.e.
        # without folder. The eyelink_data_file must be at most eight characters
        # and end with a `.edf` extension.
        self.local_data_file = data_file
        self.eyelink_data_file = os.path.basename(data_file)
        stem, ext = os.path.splitext(self.eyelink_data_file)
        if len(stem) > 8 or ext.lower() != '.edf':
            raise Exception(
                "The EyeLink cannot handle filenames longer than eight "
                "characters (excluding '.edf' extension).")

        # properties
        self.display = display
        self.fontsize = 18
        self.scr = Screen(disptype=DISPTYPE, mousevisible=False)
        self.kb = Keyboard(keylist=["escape", "q"], timeout=1)
        self.resolution = resolution
        self.recording = False
        self.saccade_velocity_treshold = saccade_velocity_threshold
        self.saccade_acceleration_treshold = saccade_acceleration_threshold
        self.eye_used = None
        self.left_eye = 0
        self.right_eye = 1
        self.binocular = 2
        self.pupil_size_mode = pupil_size_mode
        self.prevsample = (-1, -1)
        self.prevps = -1

        # event detection properties
        # degrees; maximal distance from fixation start (if gaze wanders beyond
        # this, fixation has stopped)
        self.fixtresh = 1.5
        # milliseconds; amount of time gaze has to linger within self.fixtresh
        # to be marked as a fixation
        self.fixtimetresh = 100
        # degrees per second; saccade velocity threshold
        self.spdtresh = self.saccade_velocity_treshold
        # degrees per second**2; saccade acceleration threshold
        self.accthresh = self.saccade_acceleration_treshold
        self.set_detection_type(eventdetection)
        # weighted distance, used for determining whether a movement is due to
        # measurement error (1 is ok, higher is more conservative and will
        # result in only larger saccades to be detected)
        self.weightdist = 10
        # distance between participant and screen in cm
        self.screendist = SCREENDIST
        # distance between participant and screen in cm
        self.screensize = SCREENSIZE
        self.pixpercm = (self.resolution[0]/float(self.screensize[0]) + \
         self.resolution[1]/float(self.screensize[1])) / 2.0
        # only initialize eyelink once
        if _eyelink == None:
            try:
                _eyelink = pylink.EyeLink()
            except:
                raise Exception(
                    "Error in libeyelink.libeyelink.__init__(): Failed to "
                    "connect to the tracker!")
        # determine software version of tracker
        self.tracker_software_ver = 0
        self.eyelink_ver = pylink.getEYELINK().getTrackerVersion()
        if self.eyelink_ver == 3:
            tvstr = pylink.getEYELINK().getTrackerVersionString()
            vindex = tvstr.find("EYELINK CL")
            self.tracker_software_ver = int(float(tvstr[(vindex + \
             len("EYELINK CL")):].strip()))
        if self.eyelink_ver == 1:
            self.eyelink_model = 'EyeLink I'
        elif self.eyelink_ver == 2:
            self.eyelink_model = 'EyeLink II'
        elif self.eyelink_ver == 3:
            self.eyelink_model = 'EyeLink 1000'
        else:
            self.eyelink_model = 'EyeLink (model unknown)'
        # Open graphics
        self.eyelink_graphics = EyelinkGraphics(self, _eyelink)
        pylink.openGraphicsEx(self.eyelink_graphics)
        # Optionally force drift correction. For some reason this must be done
        # as (one of) the first things, otherwise a segmentation fault occurs.
        if force_drift_correct:
            self.send_command('driftcorrect_cr_disable = OFF')
        # Set pupil-size mode
        if self.pupil_size_mode == 'area':
            pylink.getEYELINK().setPupilSizeDiameter(False)
        elif self.pupil_size_mode == 'diameter':
            pylink.getEYELINK().setPupilSizeDiameter(True)
        else:
            raise Exception(
             "pupil_size_mode should be 'area' or 'diameter', not %s" \
             % self.pupil_size_mode)
        pylink.getEYELINK().openDataFile(self.eyelink_data_file)
        pylink.flushGetkeyQueue()
        pylink.getEYELINK().setOfflineMode()
        # notify eyelink of display resolution
        self.send_command("screen_pixel_coords = 0 0 %d %d" % \
         (self.resolution[0], self.resolution[1]))
        # get some configuration stuff
        if self.eyelink_ver >= 2:
            self.send_command("select_parser_configuration 0")
            if self.eyelink_ver == 2:  # turn off scenelink camera stuff
                self.send_command("scene_camera_gazemap = NO")
        # set EDF file contents (this specifies which data is written to the EDF
        # file)
        self.send_command(
            "file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON"
        )
        if self.tracker_software_ver >= 4:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "file_sample_data  = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS")
        # set link data (this specifies which data is sent through the link and
        # thus can be used in gaze contingent displays)
        self.send_command(
            "link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON")
        if self.tracker_software_ver >= 4:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,HTARGET"
            )
        else:
            self.send_command(
                "link_sample_data  = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS")
        # not quite sure what this means (according to Sebastiaan Mathot, it
        # might be the button that is used to end drift correction?)
        self.send_command("button_function 5 'accept_target_fixation'")

        if not self.connected():
            raise Exception(
                "Error in libeyelink.libeyelink.__init__(): Failed to connect "
                "to the eyetracker!")

    def send_command(self, cmd):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        pylink.getEYELINK().sendCommand(cmd)

    def log(self, msg):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        pylink.getEYELINK().sendMessage(msg)

    def log_var(self, var, val):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        pylink.getEYELINK().sendMessage("var %s %s" % (var, val))

    def status_msg(self, msg):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        print('status message: %s' % msg)
        pylink.getEYELINK().sendCommand("record_status_message '%s'" % msg)

    def connected(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        return pylink.getEYELINK().isConnected()

    def calibrate(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.calibrate(): Trying to "
                "calibrate after recording has started!")

        # # # # #
        # EyeLink calibration and validation

        # attempt calibrate; confirm abort when esc pressed
        while True:
            self.eyelink_graphics.esc_pressed = False
            pylink.getEYELINK().doTrackerSetup()
            if not self.eyelink_graphics.esc_pressed:
                break
            self.confirm_abort_experiment()

        # If we are using the built-in EyeLink event detection, we don't need
        # the RMS calibration routine.
        if self.eventdetection == 'native':
            return

        # # # # #
        # RMS calibration

        # present instructions
        self.display.fill()  # clear display
        self.scr.draw_text(text= \
         "Noise calibration: please look at the dot\n\n(press space to start)",
         pos=(self.resolution[0]/2, int(self.resolution[1]*0.2)),
         center=True, fontsize=self.fontsize)
        self.scr.draw_fixation(fixtype='dot')
        self.display.fill(self.scr)
        self.display.show()
        self.scr.clear()  # clear screen again

        # wait for spacepress
        self.kb.get_key(keylist=['space'], timeout=None)

        # start recording
        self.log("PYGAZE RMS CALIBRATION START")
        self.start_recording()

        # show fixation
        self.display.fill()
        self.scr.draw_fixation(fixtype='dot')
        self.display.fill(self.scr)
        self.display.show()
        self.scr.clear()

        # wait for a bit, to allow participant to fixate
        clock.pause(500)

        # get samples
        # samplelist, prefilled with 1 sample to prevent sl[-1] from producing
        # an error; first sample will be ignored for RMS calculation
        sl = [self.sample()]
        t0 = clock.get_time()  # starting time
        while clock.get_time() - t0 < 1000:
            s = self.sample()  # sample
            if s != sl[-1] and s != (-1, -1) and s != (0, 0):
                sl.append(s)

        # stop recording
        self.log("PYGAZE RMS CALIBRATION END")
        self.stop_recording()

        # calculate RMS noise
        Xvar = []
        Yvar = []
        for i in range(2, len(sl)):
            Xvar.append((sl[i][0] - sl[i - 1][0])**2)
            Yvar.append((sl[i][1] - sl[i - 1][1])**2)
        XRMS = (sum(Xvar) / len(Xvar))**0.5
        YRMS = (sum(Yvar) / len(Yvar))**0.5
        self.pxdsttresh = (XRMS, YRMS)

        # recalculate thresholds (degrees to pixels)
        self.pxfixtresh = deg2pix(self.screendist, self.fixtresh,
                                  self.pixpercm)
        self.pxspdtresh = deg2pix(
            self.screendist, self.spdtresh,
            self.pixpercm) / 1000.0  # in pixels per millisecons
        self.pxacctresh = deg2pix(
            self.screendist, self.accthresh,
            self.pixpercm) / 1000.0  # in pixels per millisecond**2

    def drift_correction(self, pos=None, fix_triggered=False):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.drift_correction(): Trying to "
                "perform drift correction after recording has started!")
        if not self.connected():
            raise Exception(
                "Error in libeyelink.libeyelink.drift_correction(): The "
                "eyelink is not connected!")
        if pos == None:
            pos = self.resolution[0] / 2, self.resolution[1] / 2
        if fix_triggered:
            return self.fix_triggered_drift_correction(pos)
        return self.manual_drift_correction(pos)

    def manual_drift_correction(self, pos):
        """
		Performs a manual, i.e. spacebar-triggered drift correction.

		Arguments:
		pos		--	The positionf or the drift-correction target.

		Returns:
		True if drift correction was successfull, False otherwise.
		"""

        self.draw_drift_correction_target(pos[0], pos[1])
        self.eyelink_graphics.esc_pressed = False
        try:
            # The 0 parameters indicate that the display should not be cleared
            # and we should not be allowed to fall back to the set-up screen.
            error = pylink.getEYELINK().doDriftCorrect(pos[0], pos[1], 0, 0)
        except:
            error = -1
        # A 0 exit code means successful drift correction
        if error == 0:
            return True
        # If escape was pressed, we present the confirm abort screen
        if self.eyelink_graphics.esc_pressed:
            self.confirm_abort_experiment()
        # If 'q' was pressed, we drop back to the calibration screen
        else:
            self.calibrate()
        return False

    def prepare_drift_correction(self, pos):
        """Puts the tracker in drift correction mode"""

        # start collecting samples in drift correction mode
        self.send_command("heuristic_filter = ON")
        self.send_command("drift_correction_targets = %d %d" % pos)
        self.send_command("start_drift_correction data = 0 0 1 0")
        pylink.msecDelay(50)
        # wait for a bit until samples start coming in (again, not sure if this
        # is indeed what's going on)
        if not pylink.getEYELINK().waitForBlockStart(100, 1, 0):
            print(
                "WARNING libeyelink.libeyelink.prepare_drift_correction(): "
                "Failed to perform drift correction (waitForBlockStart error)")

    def fix_triggered_drift_correction(self,
                                       pos=None,
                                       min_samples=30,
                                       max_dev=60,
                                       reset_threshold=10):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.fix_triggered_drift_correction(): "
                "Trying to perform drift correction after recording has started!"
            )

        self.recording = True
        if pos == None:
            pos = self.resolution[0] / 2, self.resolution[1] / 2
        self.prepare_drift_correction(pos)
        self.draw_drift_correction_target(pos[0], pos[1])

        # loop until we have enough samples
        lx = []
        ly = []
        while len(lx) < min_samples:

            # Check whether the EyeLink is put into set-up mode on the EyeLink
            # PC and, if so, jump to the calibration menu.
            if pylink.getEYELINK().getCurrentMode() == pylink.IN_SETUP_MODE:
                self.recording = False
                self.calibrate()
                print(
                    "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                    "'q' pressed")
                return False

            # pressing escape enters the calibration screen
            resp = self.kb.get_key(keylist=["escape", "q"], timeout=1)[0]
            if resp == 'escape':
                self.recording = False
                self.confirm_abort_experiment()
                print(
                    "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                    "'escape' pressed")
                return False
            elif resp == 'q':
                self.recording = False
                self.calibrate()
                print(
                    "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                    "'q' pressed")
                return False
            # collect a sample
            x, y = self.sample()
            if len(lx) == 0 or x != lx[-1] or y != ly[-1]:
                # if present sample deviates too much from previous sample,
                # start from scratch.
                if len(lx) > 0 and (abs(x - lx[-1]) > reset_threshold or \
                 abs(y - ly[-1]) > reset_threshold):
                    lx = []
                    ly = []
                # Collect a sample
                else:
                    lx.append(x)
                    ly.append(y)
            # If we have enough samples to perform a drift correction ...
            if len(lx) == min_samples:
                avg_x = sum(lx) / len(lx)
                avg_y = sum(ly) / len(ly)
                d = ((avg_x - pos[0])**2 + (avg_y - pos[1])**2)**0.5
                # emulate spacebar press on succes
                pylink.getEYELINK().sendKeybutton(32, 0, pylink.KB_PRESS)
                # getCalibrationResult() returns 0 on success and an exception
                # or a non-zero value otherwise
                result = -1
                try:
                    result = pylink.getEYELINK().getCalibrationResult()
                except:
                    lx = []
                    ly = []
                    print(
                        "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                        "try again")
                if result != 0:
                    try:
                        result = pylink.getEYELINK().getCalibrationResult()
                    except:
                        lx = []
                        ly = []
                        print(
                            "libeyelink.libeyelink.fix_triggered_drift_correction(): "
                            "try again")
        # apply drift correction
        pylink.getEYELINK().applyDriftCorrect()
        self.recording = False
        print(
            "libeyelink.libeyelink.fix_triggered_drift_correction(): success")
        return True

    def start_recording(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.recording = True
        i = 0
        while True:
            # params: write samples, write event, send samples, send events
            print(u'starting recording ...')
            error = pylink.getEYELINK().startRecording(1, 1, 1, 1)
            print(u'returned %s' % error)
            if not error:
                break
            if i > self.MAX_TRY:
                raise Exception(
                    "Error in libeyelink.libeyelink.start_recording(): Failed "
                    "to start recording!")
                self.close()
                clock.expend()
            i += 1
            print(
                ("WARNING libeyelink.libeyelink.start_recording(): Failed to "
                 "start recording (attempt %d of %d)") % (i, self.MAX_TRY))
            pylink.msecDelay(100)
        # don't know what this is
        print(u'Start realtime mode ...')
        pylink.msecDelay(100)
        pylink.beginRealTimeMode(100)
        # wait a bit until samples start coming in
        print(u'Wait for block start ...')
        pylink.msecDelay(100)
        if not pylink.getEYELINK().waitForBlockStart(100, 1, 0):
            raise Exception(
                "Error in libeyelink.libeyelink.start_recording(): Failed to "
                "start recording (waitForBlockStart error)!")
        print(u'done ...')

    def stop_recording(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        print(u'stopping recording ...')
        self.recording = False
        pylink.endRealTimeMode()
        pylink.getEYELINK().setOfflineMode()
        pylink.msecDelay(500)
        print(u'done ...')

    def close(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.eyelink_graphics.close()
        if self.recording:
            self.stop_recording()
        # close data file and transfer it to the experimental PC
        print("libeyelink.libeyelink.close(): Closing data file")
        pylink.getEYELINK().closeDataFile()
        pylink.msecDelay(500)
        print("libeyelink.libeyelink.close(): Transferring %s to %s" \
         % (self.eyelink_data_file, self.local_data_file))
        pylink.getEYELINK().receiveDataFile(self.eyelink_data_file,
                                            self.local_data_file)
        pylink.msecDelay(500)
        print("libeyelink.libeyelink.close(): Closing eyelink")
        pylink.getEYELINK().close()
        pylink.msecDelay(500)

    def set_eye_used(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.eye_used = pylink.getEYELINK().eyeAvailable()
        if self.eye_used == self.right_eye:
            self.log_var("eye_used", "right")
        elif self.eye_used == self.left_eye or self.eye_used == self.binocular:
            self.log_var("eye_used", "left")
            self.eye_used = self.left_eye
        else:
            print("WARNING libeyelink.libeyelink.set_eye_used(): Failed to "
                  "determine which eye is being recorded")

    def pupil_size(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if not self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.pupil_size(): Recording was "
                "not started before collecting eyelink data!")
        if self.eye_used == None:
            self.set_eye_used()
        # get newest sample
        s = pylink.getEYELINK().getNewestSample()
        # check if sample is new
        if s != None:
            # right eye
            if self.eye_used == self.right_eye and s.isRightSample():
                ps = s.getRightEye().getPupilSize()
            # left eye
            elif self.eye_used == self.left_eye and s.isLeftSample():
                ps = s.getLeftEye().getPupilSize()
            # invalid
            else:
                ps = -1
            # set new pupil size as previous pupil size
            self.prevps = ps
        # if no new sample is available, use old data
        else:
            ps = self.prevps
        return ps

    def sample(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if not self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.sample(): Recording was not "
                "started before collecting eyelink data!")
        if self.eye_used == None:
            self.set_eye_used()
        s = pylink.getEYELINK().getNewestSample()
        if s != None:
            if self.eye_used == self.right_eye and s.isRightSample():
                gaze = s.getRightEye().getGaze()
            elif self.eye_used == self.left_eye and s.isLeftSample():
                gaze = s.getLeftEye().getGaze()
            else:
                gaze = (-1, -1)
            self.prevsample = gaze[:]
        else:
            gaze = self.prevsample[:]
        return gaze

    def set_detection_type(self, eventdetection):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if eventdetection in ['pygaze', 'native']:
            self.eventdetection = eventdetection

        return (self.eventdetection, self.eventdetection, self.eventdetection)

    def _get_eyelink_clock_async(self):
        """
		Retrieve time differenece between tracker timestamps and 
		current clock time upheld in the pygaze environment.
		
		Note that this is not guaranteed to be a static time difference, the 
		clocks might run at different speeds. Therefore you should consider 
		running this function every time you utilize on this time difference.

		Returns:
		The tracker time minus the clock time
		"""
        return pylink.getEYELINK().trackerTime() - clock.time()

    def wait_for_event(self, event):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        if not self.recording:
            raise Exception(
                "Error in libeyelink.libeyelink.wait_for_event(): Recording "
                "was not started before collecting eyelink data!")

        if self.eye_used == None:
            self.set_eye_used()
        if self.eventdetection == 'native':
            # since the link buffer was not have been polled, old data has
            # accumulated in the buffer -- so ignore events that are old:
            t0 = clock.time()  # time of call
            while True:
                d = pylink.getEYELINK().getNextData()
                if d == event:
                    float_data = pylink.getEYELINK().getFloatData()
                    # corresponding clock_time
                    tc = float_data.getTime() - self._get_eyelink_clock_async()
                    if tc > t0:
                        return tc, float_data

        if event == 5:
            outcome = self.wait_for_saccade_start()
        elif event == 6:
            outcome = self.wait_for_saccade_end()
        elif event == 7:
            outcome = self.wait_for_fixation_start()
        elif event == 8:
            outcome = self.wait_for_fixation_end()
        elif event == 3:
            outcome = self.wait_for_blink_start()
        elif event == 4:
            outcome = self.wait_for_blink_end()
        else:
            raise Exception(
                ("Error in libeyelink.libeyelink.wait_for_event: eventcode %s "
                 "is not supported") % event)
        return outcome

    def wait_for_saccade_start(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.STARTSACC)
            return t, d.getStartGaze()

        # # # # #
        # PyGaze method

        else:

            # get starting position (no blinks)
            newpos = self.sample()
            while not self.is_valid_sample(newpos):
                newpos = self.sample()
            # get starting time, position, intersampledistance, and velocity
            t0 = clock.get_time()
            prevpos = newpos[:]
            s = 0
            v0 = 0

            # get samples
            saccadic = False
            while not saccadic:
                # get new sample
                newpos = self.sample()
                t1 = clock.get_time()
                if self.is_valid_sample(newpos) and newpos != prevpos:
                    # check if distance is larger than precision error
                    sx = newpos[0] - prevpos[0]
                    sy = newpos[1] - prevpos[1]
                    # weigthed distance: (sx/tx)**2 + (sy/ty)**2 > 1 means
                    # movement larger than RMS noise
                    if (sx/self.pxdsttresh[0])**2 + (sy/self.pxdsttresh[1])**2 \
                     > self.weightdist:
                        # calculate distance
                        # intersampledistance = speed in pixels/ms
                        s = ((sx)**2 + (sy)**2)**0.5
                        # calculate velocity
                        v1 = s / (t1 - t0)
                        # calculate acceleration
                        a = (v1 - v0) / (t1 - t0
                                         )  # acceleration in pixels/ms**2
                        # check if either velocity or acceleration are above
                        # threshold values
                        if v1 > self.pxspdtresh or a > self.pxacctresh:
                            saccadic = True
                            spos = prevpos[:]
                            stime = clock.get_time()
                        # update previous values
                        t0 = copy.copy(t1)
                        v0 = copy.copy(v1)
                    # udate previous sample
                    prevpos = newpos[:]
            return stime, spos

    def wait_for_saccade_end(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.ENDSACC)
            return t, d.getStartGaze(), d.getEndGaze()

        # # # # #
        # PyGaze method

        else:

            # get starting position (no blinks)
            t0, spos = self.wait_for_saccade_start()
            # get valid sample
            prevpos = self.sample()
            while not self.is_valid_sample(prevpos):
                prevpos = self.sample()
            # get starting time, intersample distance, and velocity
            t1 = clock.get_time()
            # = intersample distance = speed in px/sample
            s = ((prevpos[0] - spos[0])**2 + (prevpos[1] - spos[1])**2)**0.5
            v0 = s / (t1 - t0)
            # run until velocity and acceleration go below threshold
            saccadic = True
            while saccadic:
                # get new sample
                newpos = self.sample()
                t1 = clock.get_time()
                if self.is_valid_sample(newpos) and newpos != prevpos:
                    # calculate distance
                    # = speed in pixels/sample
                    s = ((newpos[0]-prevpos[0])**2 + \
                     (newpos[1]-prevpos[1])**2)**0.5
                    # calculate velocity
                    v1 = s / (t1 - t0)
                    # calculate acceleration
                    # acceleration in pixels/sample**2 (actually is
                    # v1-v0 / t1-t0; but t1-t0 = 1 sample)
                    a = (v1 - v0) / (t1 - t0)
                    # check if velocity and acceleration are below threshold
                    if v1 < self.pxspdtresh and (a > -1*self.pxacctresh and \
                     a < 0):
                        saccadic = False
                        epos = newpos[:]
                        etime = clock.get_time()
                    # update previous values
                    t0 = copy.copy(t1)
                    v0 = copy.copy(v1)
                # udate previous sample
                prevpos = newpos[:]

            return etime, spos, epos

    def wait_for_fixation_start(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.STARTFIX)
            return t, d.getTime(), d.getStartGaze()

        # # # # #
        # PyGaze method

        else:

            # function assumes a 'fixation' has started when gaze position
            # remains reasonably stable for self.fixtimetresh

            # get starting position
            spos = self.sample()
            while not self.is_valid_sample(spos):
                spos = self.sample()

            # get starting time
            t0 = clock.get_time()

            # wait for reasonably stable position
            moving = True
            while moving:
                # get new sample
                npos = self.sample()
                # check if sample is valid
                if self.is_valid_sample(npos):
                    # check if new sample is too far from starting position
                    if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > \
                     self.pxfixtresh**2: # Pythagoras
                        # if not, reset starting position and time
                        spos = copy.copy(npos)
                        t0 = clock.get_time()
                    # if new sample is close to starting sample
                    else:
                        # get timestamp
                        t1 = clock.get_time()
                        # check if fixation time threshold has been surpassed
                        if t1 - t0 >= self.fixtimetresh:
                            # return time and starting position
                            return t1, spos

    def wait_for_fixation_end(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.ENDFIX)
            return t, d.getTime(), d.getStartGaze()

        # # # # #
        # PyGaze method

        else:

            # function assumes that a 'fixation' has ended when a deviation of
            # more than fixtresh from the initial 'fixation' position has been
            # detected

            # get starting time and position
            stime, spos = self.wait_for_fixation_start()

            # loop until fixation has ended
            while True:
                # get new sample
                npos = self.sample()  # get newest sample
                # check if sample is valid
                if self.is_valid_sample(npos):
                    # check if sample deviates to much from starting position
                    if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > \
                     self.pxfixtresh**2: # Pythagoras
                        # break loop if deviation is too high
                        break

            return clock.get_time(), spos

    def wait_for_blink_start(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.STARTBLINK)
            return t, d.getTime()

        # # # # #
        # PyGaze method

        else:

            blinking = False

            # loop until there is a blink
            while not blinking:
                # get newest sample
                gazepos = self.sample()
                # check if it's a valid sample
                if not self.is_valid_sample(gazepos):
                    # get timestamp for possible blink start
                    t0 = clock.get_time()
                    # loop until a blink is determined, or a valid sample occurs
                    while not self.is_valid_sample(self.sample()):
                        # check if time has surpassed 150 ms
                        if clock.get_time() - t0 >= 150:
                            # return timestamp of blink start
                            return t0

    def wait_for_blink_end(self):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        # # # # #
        # EyeLink method

        if self.eventdetection == 'native':
            t, d = self.wait_for_event(pylink.ENDBLINK)
            return t

        # # # # #
        # PyGaze method

        else:

            blinking = True

            # loop while there is a blink
            while blinking:
                # get newest sample
                gazepos = self.sample()
                # check if it's valid
                if self.is_valid_sample(gazepos):
                    # if it is a valid sample, blinking has stopped
                    blinking = False

            # return timestamp of blink end
            return clock.get_time()

    def set_draw_calibration_target_func(self, func):
        """See pygaze._eyetracker.baseeyetracker.BaseEyeTracker"""

        self.eyelink_graphics.draw_cal_target = func

    # ***
    #
    # Internal functions below
    #
    # ***

    def is_valid_sample(self, gazepos):
        """
		Checks if the sample provided is valid, based on EyeLink specific
		criteria.
		
		arguments
		gazepos		--	a (x,y) gaze position tuple, as returned by
						self.sample()
		
		returns
		valid			--	a Boolean: True on a valid sample, False on
						an invalid sample
		"""

        # return False if a sample is invalid
        if gazepos == (-1, -1):
            return False

        # in any other case, the sample is valid
        return True

    def confirm_abort_experiment(self):
        """
		Asks for confirmation before aborting the experiment. Displays a
		confirmation screen, collects the response, and acts accordingly.

		Exceptions:
		Raises a response_error upon confirmation.

		Returns:
		False if no confirmation was given.
		"""

        # Display the confirmation screen
        scr = Screen(disptype=DISPTYPE)
        kb = Keyboard(timeout=5000)
        yc = DISPSIZE[1] / 2
        xc = DISPSIZE[0] / 2
        ld = 40  # Line height
        scr.draw_text(
            u'Really abort experiment?',
            pos=(xc, yc - 3 * ld),
            fontsize=self.fontsize)
        scr.draw_text(
            u'Press \'Y\' to abort',
            pos=(xc, yc - 0.5 * ld),
            fontsize=self.fontsize)
        scr.draw_text(
            u'Press any other key or wait 5s to go to setup',
            pos=(xc, yc + 0.5 * ld),
            fontsize=self.fontsize)
        self.display.fill(scr)
        self.display.show()
        # process the response:
        try:
            key, time = kb.get_key()
        except:
            return False
        # if confirmation, close experiment
        if key == u'y':
            raise Exception(u'The experiment was aborted')
        self.eyelink_graphics.esc_pressed = False
        return False

    def draw_drift_correction_target(self, x, y):
        """
		Draws the drift-correction target.
		
		arguments
		
		x		--	The X coordinate
		y		--	The Y coordinate
		"""

        self.scr.clear()
        self.scr.draw_fixation(fixtype='dot', colour=FGC, pos=(x,y), pw=0, \
         diameter=12)
        self.display.fill(self.scr)
        self.display.show()