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.
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:
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.
Changing a Device Event buffer (e.g., getEvents() or clearEvents() does not influence the Global Event buffer, and vice versa.
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:
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.
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:
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: