diff --git a/psychopy_eyetracker_pupil_labs/classic/neon_event.png b/psychopy_eyetracker_pupil_labs/classic/neon_event.png new file mode 100644 index 0000000..d245dce Binary files /dev/null and b/psychopy_eyetracker_pupil_labs/classic/neon_event.png differ diff --git a/psychopy_eyetracker_pupil_labs/classic/neon_event@2x.png b/psychopy_eyetracker_pupil_labs/classic/neon_event@2x.png new file mode 100644 index 0000000..474d67d Binary files /dev/null and b/psychopy_eyetracker_pupil_labs/classic/neon_event@2x.png differ diff --git a/psychopy_eyetracker_pupil_labs/dark/neon_event.png b/psychopy_eyetracker_pupil_labs/dark/neon_event.png new file mode 100644 index 0000000..fa4aa5a Binary files /dev/null and b/psychopy_eyetracker_pupil_labs/dark/neon_event.png differ diff --git a/psychopy_eyetracker_pupil_labs/dark/neon_event@2x.png b/psychopy_eyetracker_pupil_labs/dark/neon_event@2x.png new file mode 100644 index 0000000..932a16c Binary files /dev/null and b/psychopy_eyetracker_pupil_labs/dark/neon_event@2x.png differ diff --git a/psychopy_eyetracker_pupil_labs/light/neon_event.png b/psychopy_eyetracker_pupil_labs/light/neon_event.png new file mode 100644 index 0000000..d245dce Binary files /dev/null and b/psychopy_eyetracker_pupil_labs/light/neon_event.png differ diff --git a/psychopy_eyetracker_pupil_labs/light/neon_event@2x.png b/psychopy_eyetracker_pupil_labs/light/neon_event@2x.png new file mode 100644 index 0000000..474d67d Binary files /dev/null and b/psychopy_eyetracker_pupil_labs/light/neon_event@2x.png differ diff --git a/psychopy_eyetracker_pupil_labs/pupil_labs/components.py b/psychopy_eyetracker_pupil_labs/pupil_labs/components.py index 9c1cff1..d6dd42e 100644 --- a/psychopy_eyetracker_pupil_labs/pupil_labs/components.py +++ b/psychopy_eyetracker_pupil_labs/pupil_labs/components.py @@ -1,6 +1,6 @@ from pathlib import Path -from psychopy.experiment.components import BaseVisualComponent, Param, getInitVals +from psychopy.experiment.components import BaseVisualComponent, BaseComponent, Param, getInitVals from psychopy.localization import _translate @@ -13,8 +13,8 @@ class AprilTagComponent(BaseVisualComponent): _instances = [] _routine_start_written = False - def __init__(self, exp, parentName, marker_id=0, anchor="center", size=(0.2, 0.2), startType='time (s)', startVal=0.0, *args, **kwargs): - super().__init__(exp, parentName, size=size, startType=startType, startVal=startVal, *args, **kwargs) + def __init__(self, exp, parentName, name='aprilTag', marker_id=0, anchor="center", size=(0.2, 0.2), startType='time (s)', startVal=0.0, *args, **kwargs): + super().__init__(exp, parentName, name=name, size=size, startType=startType, startVal=startVal, *args, **kwargs) self.type = 'Image' self.url = "https://april.eecs.umich.edu/software/apriltag.html" @@ -109,8 +109,8 @@ class AprilTagFrameComponent(BaseVisualComponent): iconFile = Path(__file__).parent.parent / 'apriltag_frame.png' tooltip = _translate('AprilTag: Markers to identify a screen surface') - def __init__(self, exp, parentName, h_count=4, v_count=3, marker_ids='', marker_size=0.125, marker_units="from exp settings", anchor="center", size=[2, 2], units="norm", startType='time (s)', startVal=0.0, *args, **kwargs): - super().__init__(exp, parentName, size=size, units=units, startType=startType, startVal=startVal, *args, **kwargs) + def __init__(self, exp, parentName, name='tagFrame', h_count=4, v_count=3, marker_ids='', marker_size=0.125, marker_units="from exp settings", anchor="center", size=[2, 2], units="norm", startType='time (s)', startVal=0.0, *args, **kwargs): + super().__init__(exp, parentName, name=name, size=size, units=units, startType=startType, startVal=startVal, *args, **kwargs) self.type = 'Image' self.url = "https://april.eecs.umich.edu/software/apriltag.html" @@ -210,3 +210,41 @@ def writeRoutineStartCode(self, buff): f" eyetracker.register_surface({inits['name']}.marker_verts, win_size_pix)\n") buff.writeIndentedLines(code) + + +class NeonEventComponent(BaseComponent): + targets = ['PsychoPy'] + categories = ['Eyetracking'] + iconFile = Path(__file__).parent.parent / 'neon_event.png' + tooltip = _translate('Save a timestamped event in a Neon recording') + + def __init__(self, exp, parentName, name='neonEvent', event_name='Event 1', timestamp_ns=0, startType='time (s)', startVal=0.0, stopType='duration (s)', stopVal=1.0, *args, **kwargs): + super().__init__(exp, parentName, name=name, startType=startType, startVal=startVal, stopType=stopType, stopVal=stopVal, *args, **kwargs) + + self.url = "https://docs.pupil-labs.com/neon/data-collection/events/" + self.exp.requireImport('BasicComponent', 'psychopy_eyetracker_pupil_labs.pupil_labs.stimuli') + + _allow3 = ['constant', 'set every repeat', 'set every frame'] # list + self.params['event_name'] = Param( + event_name, valType='str', inputType="single", allowedTypes=[], categ='Basic', + updates='constant', allowedUpdates=_allow3[:], + hint=_translate("The name of the event to be saved"), + canBePath=False, + label=_translate("Event Name")) + + self.params['timestamp_ns'] = Param(timestamp_ns, + valType='int', categ='Basic', + updates='constant', allowedUpdates=_allow3[:], + hint=_translate("The timestamp of the event or `0` for automatic"), + label=_translate("Event timestamp (ns)")) + + def writeInitCode(self, buff): + inits = getInitVals(self.params, 'PsychoPy') + buff.writeIndentedLines(f"{inits['name']} = BasicComponent()\n") + + def writeRoutineStartCode(self, buff): + inits = getInitVals(self.params, 'PsychoPy') + code = (f"if eyetracker is not None and hasattr(eyetracker, 'send_event'):\n" + f" eyetracker.send_event({inits['event_name']}, {inits['timestamp_ns']})\n") + + buff.writeIndentedLines(code) diff --git a/psychopy_eyetracker_pupil_labs/pupil_labs/neon/eyetracker.py b/psychopy_eyetracker_pupil_labs/pupil_labs/neon/eyetracker.py index bae2704..6a0584f 100644 --- a/psychopy_eyetracker_pupil_labs/pupil_labs/neon/eyetracker.py +++ b/psychopy_eyetracker_pupil_labs/pupil_labs/neon/eyetracker.py @@ -285,7 +285,6 @@ def _poll(self): gaze = self._device.receive_gaze_datum(timeout_seconds=0) if gaze is not None and hasattr(gaze, 'pupil_diameter_left'): - print('add pupil sample') self._add_pupil_sample(gaze, logged_time) def _add_gaze_sample(self, surface_gaze, gaze_datum, logged_time): @@ -451,6 +450,12 @@ def register_surface(self, tag_verts, window_size): self._window_size = window_size + def send_event(self, event_name, timestamp_ns): + if timestamp_ns == 0: + timestamp_ns = None + + self._device.send_event(event_name, event_timestamp_unix_ns=timestamp_ns) + def _psychopyTimeInTrackerTime(self, psychopy_time): return psychopy_time + self._time_offset_estimate.time_offset_ms.mean / 1000 diff --git a/psychopy_eyetracker_pupil_labs/pupil_labs/stimuli.py b/psychopy_eyetracker_pupil_labs/pupil_labs/stimuli.py index 6d2eaed..ac94865 100644 --- a/psychopy_eyetracker_pupil_labs/pupil_labs/stimuli.py +++ b/psychopy_eyetracker_pupil_labs/pupil_labs/stimuli.py @@ -6,6 +6,8 @@ from pupil_labs.real_time_screen_gaze import marker_generator +from psychopy.tools.attributetools import AttributeGetSetMixin + class AprilTagStim(ImageStim): def __init__(self, marker_id=0, contrast=1.0, *args, **kwargs): @@ -124,3 +126,7 @@ def _frame_positions(self, frame_size, marker_size, grid_counts): ]) return spacing * np.array(self._frame_grid(*grid_counts)) + + +class BasicComponent(AttributeGetSetMixin): + pass diff --git a/pyproject.toml b/pyproject.toml index 864fe6e..30e3bb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,3 +62,4 @@ pupil_core = "psychopy_eyetracker_pupil_labs.pupil_labs.pupil_core" [project.entry-points."psychopy.experiment.components"] AprilTagComponent = "psychopy_eyetracker_pupil_labs.pupil_labs.components:AprilTagComponent" AprilTagFrameComponent = "psychopy_eyetracker_pupil_labs.pupil_labs.components:AprilTagFrameComponent" +NeonEventComponent = "psychopy_eyetracker_pupil_labs.pupil_labs.components:NeonEventComponent"