The ioHub Event Model

The core functionality of the ioHub Event Monitoring Framework is a unique way to manage device events. A understanding of the ioHub Event Model is therefore very useful for users wanting to maximize the benefits of utilizing ioHub. Below is a brief description of that model, including how events are buffered, ways of fetching and clearing event data, and different reasons to interact with the ioHub DeviceEvent buffers in real-time.

ioHub Event Buffers

The ioHub Process buffers, or stores, events received from monitored devices to be available to the PsychoPy process when desired in the experiment script. ioHub Event Buffers are always implemented as circular buffers (using the Python collections.deque class), which allows the ioHub Process to maintain a fixed maximum size in memory (defined per device type) regardless of how the DeviceEvents are used (or not used). These circular buffers allow ioHub to successfully and accurately monitor many devices without interfering with the PsychoPy experiment.

There are two levels of event buffers in the ioHub Process:

  • Device Event Buffers accessed using the the PsychoPy’s Process’s ioHubDeviceView getEvents() method. A Device Event buffer is created per user-configured device. The maximum size of a Device Event buffer is determined by the value of the ‘event_buffer_length’ configuration property of the device (usually in the iohub_config.yaml). These buffers hold events generated by the target device, and return these events sorted by time, with the oldest event first.
  • Global Event Buffer, accessed using the ioHubConnection’s getEvents() method, stores events from all devices being monitored. There is only one Global Event Buffer in the ioHub Process, holding up a mximum number of events as specified by the ‘global_event_buffer’ preference. The Global Event Buffer also returns events sorted by time, but events can be sorted across all devices. Events are sorted chronologically in real-time to make it easy to send events to the PsychoPy Process.

Note

Retrieve events from a Device Event Buffer when you need to access events from a specific device.

Retrieve events from the Global Event Buffer when you need to access the chronology of events across all devices.

Having two event buffer levels allows users to manage events from devices individually at one point in the experiment script and manage the chronology of events across devices at a different point in the experiment. Keeping the event buffer levels independent allows users to, for example, save events from all monitored devices in a custom event file format instead of the DataStore, while also using event data to control the experiment ‘states’ (like response triggers). In this case, the Global Event Buffer could save events for data management, while the Device Event Buffers could manage experiment flow control.

Note

Changing a Device Event buffer (e.g., getEvents() or clearEvents() does not influence the Global Event buffer, and vice versa.

ioHub DeviceEvent Life Cycle

A ‘native device event’ is the event as received by the ioHub Process from the underlying device interface interacting with the device hardware. The life cyle of an ioHub DeviceEvent starts when a native device event is received, and ends when the ioHub DeviceEvent that was created to represent the native event is:

  • Sent to the PsychoPy Process.
  • Cleared from the ioHub Process by the PsychoPy Process.
  • Removed from the ioHub Process because the buffer holding the event has become full is has removed the event so a newer event can be added.

The main operations that occur when a device event is ‘born’:

  • The event is time stamped using the shared ioHub / PsychoPy time base when it has not been time stamped by the source native device.

  • When an event does have a natively provided time stamp, it is converted to the shared ioHub / PsychoPy time base.

  • If possible, the time base conversion process corrects the time created for the event by factoring in any delay or drift between the device’s native time base and the ioHub time base.

  • The native data is converted into the relevant ioHub DeviceEvent format.

  • The ioHub DeviceEvent is then:

    • Added to the ioHub’s Global Event Buffer.
    • Added to the Device Buffer’s based on the event’s Parent Device.
    • Handed to the ioHub DataStore for persistant storage if the DataStore is being used.

Working with ioHub DeviceEvents

Using a Device Event Buffer

Here is an example of how to collect a reaction time from when a screen was first presented while keeping the PsychoPy window updated:

# Assumes 'io' object was created using the
# psychopy.iohub.launchHubProcess() function and
# 'window' is a full screen PsychoPy Window

# save some 'dots' during the trial loop
keyboard = io.devices.keyboard

# Store the RT calculation here
spacebar_rt=0.0

# build visual stim as needed
# ....

# Display first frame of screen
flip_time=window.flip()

io.clearEvents('all')

# Run each trial until space bar is pressed
while spacebar_rt == 0.0:
    events=keyboard.getEvents()

    for kb_event in events:
        if kb_event.key == ' ':
            spacebar_rt=kb_event.time-flip_time

    # Update visual stim as needed
    # ....

    # Display next frame of screen
    window.flip()

This example demonstrates the advantages of using Device Event Buffers in the ioHub Event Model:

  • No events that occured prior to the initial display of the stimuli will be received.
  • Only keyboard events are needed, so only keyboard events are received.
  • Events are received sorted by time, so the first ‘ ‘ key event encountered will be the KeyPress event.
  • The reaction time is calculated precisely from the moment the stimulus was shown until the moment the key was pressed, not the time the event was handled by the PsychoPy Process.

Using the Global Event Buffer

In this example, mouse events need to be handled, but after the participant presses the ‘s’ key and the first time s/he presses the ‘e’ key. Here we utilize the device-independent Global Event Buffer while also keeping the PsychoPy window in an updated state:

# Assumes 'io' object was created using the
# psychopy.iohub.launchHubProcess() function and
# 'window' is a full screen PsychoPy Window

# store the 's' key event and 'e' key events in these objects.
s_event=None
e_event=None

# build visual stim as needed
# ....
flip_time=window.flip()

io.clearEvents('all')

while you_want_to_run_the_trial:
    events=io.getEvents()

    while s_event is None and events:
        event = events.pop(0)
        if event.type == EventConstants.KEYBOARD_KEY and event.key == 's':
            s_event=event

    while events and s_event and not e_event:
        event = events.pop(0)
        if event.type == EventConstants.MOUSE_MOVE:
            # do as you will with the mouse event....
            # i.e.
            time_since_s_pressed=event.time-s_event.time

        elif event.type == EventConstants.KEYBOARD_KEY and event.key == 'e':
            e_event=event

    # build visual stim as needed
    # ....
    flip_time=window.flip()

This example demonstrates the advantages of using Global Event Buffers in the ioHub Event Model:

  • Mouse motion events can simply be selected based on whether the ‘s’ key and ‘e’ key event have been encountered; no need to compare event time.
  • No mouse movement events will be skipped betwen the ‘s’ key event and ‘e’ key event, or when a screen update is being done.
  • If a time difference calculation is desired, it can be done based on the event time attribue, not based on when the event was received by the PsychoPy Process.