Skip to main content

eyelogic-recorder

DOI

A lightweight Python client for EyeLogic eye trackers, built on top of the official EyeLogic SDK. It adds what the SDK does not provide: asynchronous recording of every gaze sample to plain CSV, timestamped messages (for example TRIALID 3 or TRIAL_END) in a separate TSV file that shares the clock of the sample file, typed exceptions, and context-manager cleanup. It works well with PsychoPy but does not require it. The only Python dependency is NumPy (plus the Windows-only EyeLogic SDK).

Moving-window demo: text outside seven characters around the gaze (red dot) is masked

examples/gaze_contingent.py in reading mode: outside seven characters to either side of the fixated letter (red dot), letters are replaced by x.

Status

First public release (0.2.0). The recording code was reviewed by EyeLogic and tested on their hardware. Feedback and issues welcome.

Installation

pip install eyelogic-recorder

On Windows this also installs the EyeLogic SDK (eyelogic-sdk), pinned to the version eyelogic-recorder is tested with (1.2.4). The installed EyeLogic server must be at least as new as the SDK (1.2.4 or later); EyeLogic keeps newer servers compatible with older SDKs. SDK 1.2.x no longer bundles the MSVC runtime, so you may need the Microsoft Visual C++ Redistributable.

Reading recorded files (load_samples, load_messages) works on any platform; recording requires Windows and the SDK.

The development version installs from GitHub: pip install git+https://github.com/laubrock-lab/eyelogic-recorder.

Quick start

from eyelogic_recorder import EyeLogicClient, EyeLogicError

try:
    with EyeLogicClient("my_experiment", data_folder="data") as et:
        et.connect()
        et.find_tracker()
        et.start_tracking()
        et.calibrate()
        et.validate()
        for trial in trials:
            et.start_recording()
            et.send_message(f"TRIALID {trial.id}")
            ...  # present the stimulus
            et.send_message("TRIAL_END")
            et.stop_recording()
            et.check_health()
except EyeLogicError as err:
    print("Eye tracker problem:", err)

Leaving the with block (or calling close()) stops recording and tracking, disconnects, and closes the files.

Right after calibrate() the EyeLogic server is briefly busy (a known server bug, confirmed by EyeLogic), so a validate() started immediately would fail with ALREADY_BUSY. calibrate() and validate() therefore retry for up to busy_timeout seconds (default 5); no wait is needed in your own code. calibrate() also waits up to eye_timeout seconds (default 5) for a detected eye and raises NoEyeDetectedError if there is none.

A runnable version without PsychoPy is in examples/minimal.py (python examples/minimal.py --help).

Examples (all accept --help; the PsychoPy ones also --simulate --windowed to run without a tracker, with the mouse as the eye):

  • examples/minimal.py: recording without PsychoPy.
  • examples/psychopy_free_viewing.py: free viewing of images.
  • examples/paragraph_reading.py: paragraph reading with a comprehension question; word AOIs derived from TextBox2 glyph vertices and logged as messages; own texts via --texts CSV. The built-in texts are the openings of two Grimm fairy tales (1857 edition, public domain).
  • examples/gaze_contingent.py: gaze marker plus a moving window, for reading (--mode text, ±N characters on the fixated line) or scene viewing (--mode scene, circular window).
  • examples/boundary.py: boundary paradigm for reading, with a fixation check, the changed sentence pre-rendered so the display change is a pure buffer swap, and word AOIs logged as messages (based on an implementation by Philipp Freiherr von Entreß-Fürsteneck).

Running without a tracker

eyelogic_recorder.simulator replaces the EyeLogic SDK with a simulated tracker that delivers samples at the device frame rate (synthetic fixations, saccades and blinks, or any position source such as the mouse). It works on any OS:

from eyelogic_recorder import EyeLogicClient, simulator

simulator.install()  # before creating the client
with EyeLogicClient("dry_run") as et:
    ...

The same simulator drives the test suite and benchmarks/synthetic_throughput.py.

Output files

Each client creates two files in data_folder, named <experiment>_<YYYYmmdd_HHMMSS>.csv and .tsv. Existing files are never overwritten (a numeric suffix is added if needed).

Samples (.csv)

One row per gaze sample. Invalid samples (eye not tracked) are kept, with NA in the gaze and eye columns.

Column Meaning
ETTime Timestamp from the SDK (timestampMicroSec), microseconds since the epoch on the host clock
Index SDK sample index (gaps mean samples the SDK did not deliver)
x, y Binocular raw point of regard, screen pixels
x.filtered, y.filtered Binocular filtered point of regard, screen pixels
x.left, y.left Left-eye point of regard, screen pixels
eyePositionLeftX/Y/Z Left-eye position in mm, device coordinates
pr.left Left pupil radius in mm
x.right, y.right Right-eye point of regard, screen pixels
eyePositionRightX/Y/Z Right-eye position in mm, device coordinates
pr.right Right pupil radius in mm
SysTime Host time.time() (seconds) when the sample arrived in Python; same clock as the message file

Use SysTime to align samples with messages. SysTime − ETTime is not a reliable per-sample latency; for measuring gaze-contingent latency see benchmarks/gaze_contingent_latency.md.

Validity is per vector (EyeLogic): each eye's point of regard (x.left/y.left, x.right/y.right), eye position and pupil can be NA independently. The binocular point of regard (x/y, x.filtered/y.filtered) is valid when at least one eye's is. Points of regard need a calibration; eye positions and pupils only need a found eye, so before calibration they are recorded while the gaze columns are NA.

Messages (.tsv)

Tab-separated with columns SysTime, Type, Text. Type is MSG for your own messages (send_message), EVENT for device events reported by the SDK, ERROR for failures (with the reason, e.g. Validation failed: ReturnValidate.ALREADY_BUSY), WARNING for data-quality problems (e.g. a recording without any detected eye), and INFO for client status lines (connect, calibration, validation, recording start/stop). SysTime is on the same clock as the SysTime column of the sample file, so messages can be aligned with samples.

Reading the data

Python:

import eyelogic_recorder

samples = eyelogic_recorder.load_samples("data/my_experiment_20260101_120000.csv")  # structured array, NA -> NaN
messages = eyelogic_recorder.load_messages("data/my_experiment_20260101_120000.tsv")  # list of Message(sys_time, type, text)

R:

samples  <- read.csv("data/my_experiment_20260101_120000.csv")  # NA is read as missing by default
messages <- read.delim("data/my_experiment_20260101_120000.tsv")

Gaze-contingent use

get_position() returns the latest filtered gaze position (or None if the last sample was invalid) whenever tracking runs, also between recordings. get_position(as_array=True) returns a NumPy array; last_sample gives the full SDK sample (every sample, also when some of its fields are invalid).

Error model

All errors derive from EyeLogicError:

  • ConnectError, DeviceNotFoundError, StartTrackingError, CalibrationError, ValidationError: raised by the corresponding call.
  • ConnectionClosedError, DeviceDisconnectedError, TrackingStoppedError: the server connection, device, or tracking was lost unexpectedly.
  • WriterError: the background writer thread failed (for example disk full).
  • RecordingOverflowError: samples were dropped because the writer fell behind by more than the buffer pool (samples_dropped).
  • MessageWriteError: a message (e.g. a trial marker) could not be written.
  • NoEyeDetectedError: calibrate() saw no eye within eye_timeout seconds (participant not in view, or a frame rate the device offers but does not support: according to EyeLogic only the LogicOne supports 250 Hz, but an EyeLogic Lite may still offer it).

A ValidationError is also raised when validation finds no eye at any target. After each recording, the message file gets the share of samples with a detected eye; if there were none (or no samples at all), a WARNING line is written and an EyeLogicWarning is issued.

Failures that occur on SDK or writer threads cannot be raised where they happen. They are stored and raised by check_health() and by the methods that call it (connect, start_recording, get_position, ...). Call check_health() regularly, for example once per trial.

Running tests

pip install -e .[dev]
pytest

The tests use a fake SDK and run on any OS.

Design

Gaze samples are copied by the SDK callback into preallocated NumPy chunks; full chunks go to a writer thread, so disk I/O never delays the SDK callback (the EyeLogic server drops samples when the callback is slow). The chunk pool buffers 40 s at 250 Hz by default; if the disk stalls for longer, samples are dropped, counted (samples_dropped) and reported by check_health() as RecordingOverflowError instead of being lost silently. See docs/recording_pipeline.md for details.

Contributors

  • Philipp Freiherr von Entreß-Fürsteneck: the boundary-paradigm example (examples/boundary.py), adapted from Philipp's original implementation, and the high-DPI coordinate conversion used in all PsychoPy examples.
  • Aynur Pelin Çelikkol: the paragraph-reading example (examples/paragraph_reading.py), which follows the design of Pelin's reading-comprehension task (trial flow, text display with TextBox2).
  • Niklas Rosin and Joline Flatau: testing on EyeLogic hardware.

Acknowledgements

We thank Matthias Nieser and Thomas Jablonski of EyeLogic for reviewing the recording code and testing it on EyeLogic hardware. eyelogic-recorder is developed independently and is not an official EyeLogic product.

License

MIT, see LICENSE.

Citation

Please cite the software via its DOI, https://doi.org/10.5281/zenodo.23537087, which resolves to the latest version (each version also has its own DOI on Zenodo). Details are in CITATION.cff.

Metadata

Release files for eyelogic-recorder 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for eyelogic-recorder 0.2.0
File Size Uploaded Provenance
eyelogic_recorder-0.2.0.tar.gz 29.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for eyelogic-recorder 0.2.0
File Interpreter ABI Platform
eyelogic_recorder-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 52.0 kB

Release files / eyelogic_recorder-0.2.0.tar.gz

Download URL eyelogic_recorder-0.2.0.tar.gz
Size 29.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c7d554d174f24165c29b4f5f345d4d3e86193c8580ab49fb586d6dd30c22c329
BLAKE2b-256 checksum
How to use checksums
264cf31d6ff4f5d0a65d293df528be161ff85d75e426b1a7e184e8b8a349eb7f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.11

Release files / eyelogic_recorder-0.2.0-py3-none-any.whl

Download URL eyelogic_recorder-0.2.0-py3-none-any.whl
Size 22.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1e57f65cdeeb83e5f7e6b554a82dd5fa1a2652bdac21f66dc9935c0a83292c94
BLAKE2b-256 checksum
How to use checksums
b241b309122558d12442c25ca5608188e62b731c425eaeec2ec259738f87d837
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.11

Security for eyelogic-recorder 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page