Adding EEG triggers to experiment

Hello fellow psychopyers,

I want to apply EEG triggers everytime my visual stimulus (a checker pattern) or an auditory beep happens. Any help or example code on this would be greatly appreciated.

The experiment shows a 40 second video and whilst the video is playing a short visual stimulus appears at random intervals (a checker pattern) or an auditory beep. There are 6 trials in total (each trial is duration of video). Please find below the current code I am using

gligalab.psyexp (20.9 KB)

Brain products (company of the EEG I am using) has this python example code but I am not sure where all these values should be inputted. Any advice would be extremely welcomed

#Python Example
import serial
import time
import threading
Connected = True
PulseWidth = 0.01
def ReadThread(port):
  while Connected:
     if port.inWaiting() > 0:
        print "0x%X"%ord(port.read(1))
# Open the Windows device manager, search for the "TriggerBox VirtualSerial Port (COM6)"
# in "Ports /COM & LPT)" and enter the COM port number in the constructor.
port = serial.Serial("COM6")
# Start the read thread
thread = threading.Thread(target=ReadThread, args=(port,))
thread.start()
# Set the port to an initial state
port.write([0x00])
time.sleep(PulseWidth)
# Set Bit 0, Pin 2 of the Output(to Amp) connector
port.write([0x01])
time.sleep(PulseWidth)
# Reset Bit 0, Pin 2 of the Output(to Amp) connector
port.write([0x00])
time.sleep(PulseWidth)
# Reset the port to its default state
port.write([0xFF])
time.sleep(PulseWidth)
# Terminate the read thread
Connected = False
thread.join(1.0)
# Close the serial port
port.close()

Check out our c-pod product; it has PsychoPy support (see Using c-pod with PsychoPy). A version for Brain Products is available.

One key differentiator between c-pod and pretty much all the rest is its asynchronous output capability: you specify the pulse width and you’re done – no need to call time.sleep().

Another cool built-in feature IMO is the pulse table, designed specifically for scenarios such as yours: you send commands to c-pod to let it know when you want it to send pulses. In your case that would be when the checker pattern appears or the auditory beep plays. Then, when the video starts to play, you send c-pod a command to start “running” the pulse table, and event markers will be sent at the times that you pre-specified.

Our Python library does not currently have support for the pulse table, but this would be something we can quickly add.

Meanwhile, you can see more about the “raw commands” that c-pod accepts to handle the pulse table feature.

If you insert a “code” component (from the “custom” component panel), you’ll see it has tabs in which you insert code to run at different times. e.g. for things that should only be done once, put them in the “begin experiment” tab, like these initial set-up tasks:

import serial
port = serial.Serial('COM6')
port.write([0x00])

Then for things that should happen once per trial, put them in the “begin routine” tab, e.g. to initialise and reset certain variables like this:

pulse_started = False
pulse_ended = False

Then for things that should be done or checked continuously, put them in the “each frame” tab so that they run on each screen refresh (typically at 60 Hz):

if your_stimulus.status == STARTED and not pulse_started:
    port.write([0x01])
    pulse_start_time = globalClock.getTime()
    pulse_started = True

if pulse_started and not pulse_ended:
    if globalClock.getTime() - pulse_start_time >= 0.01:
        port.write([0x00])
        pulse_ended = True

This will be a bit sloppy with the duration of the pulse: it will always be at least 10 ms but will usually be longer than 10 ms, as the end of the pulse is likely to occur on the next screen refresh, which will typically be 16.7 ms later. If you want to have more precise control of the pulse width, look into suggestions like those by @habboud, or using threads.

You should avoid using time.sleep() in a Builder script: durations as long as 10 ms will likely interrupt its attempts to maintain an active drawing cycle that keeps up with the screen refresh rate, which will damage your stimulus timing and performance. But realistically, it is the timing of the onset of the pulse that is most critical, and having it be of sufficient duration to be detected. If it runs a few milliseconds longer, that will not usually be an issue at all. So in this case, we haven’t used threads or time.sleep(), at the cost of accepting that the pulse duration will be longer than 10 ms and a bit variable.

2 Likes

Thank you for your response Michael. So taking your example in my experiment I have posted above one of the stimuli that I would like to trigger is a beep that plays at random intervals.

The code should be as follows:
Begin experiment


# make tone

beep = sound.Sound('A', secs=0.1, stereo=True, hamming=True,
    name='$beep')
beep.setVolume(1)
beep.playing = False
beep.waiting = False
trialDuration = 40

import serial
port = serial.Serial('COM5')
port.write([0x00])

Begin routine


beepOnsets=[]
beepISIs=[]
pulse_started = False
pulse_ended = False

Each frame


# we want to present the tone every
# 6 - 10 seconds for the duration of the trial
# if the sound is not currently playing
if not beep.playing and not beep.waiting:
    # pick how long we will wait for
    beepISI = randint(6, 10)
    print('beepISI', beepISI)
    beepISIs.append(beepISI)
    beepOnset = t +beepISI
    #we are waiting for the sound to play
    beep.waiting = True
elif not beep.playing and beep.waiting:
    if t >= beepOnset:
        print('playing')
        beep.play()
        beepOnsets.append(t)
        beep.playing = True
        beep.waiting = False
elif beep.playing:
    if t >= beepOnset + beep.secs:
        beep.stop()
        beep.playing = False

if beep.playing == True and not pulse_started:
    port.write([0x01])
    pulse_start_time = globalClock.getTime()
    pulse_started = True

if pulse_started and not pulse_ended:
    if globalClock.getTime() - pulse_start_time >= 0.01:
        port.write([0x00])
        pulse_ended = True

With this code only the first time the stimulus appears the trigger appears on the EEG. How can I keep repeating the trigger each time the stimulus appears??

Continue what you have started, by inserting a debugging print statement inside each clause of your code. That way you can follow the logic by seeing what differs between the first time the trigger is sent and the next time, when it isn’t.

NB print statments should only be a temporary debugging tool. Once the code is working, make sure to delete them, as they can impair real-time performance.

PS having said that, only having skimmed the code (which looks good), I’d guess that beep.playing needs to be reset to False at the beginning of the routine as well.

Oh yes beep.playing reset to False did the trick, duh! Thanks Michael. One final question: What should I change so that the trigger have identifiers for different stimuli. For example in my study I also have a picture flashing at random ISIs but using the code above all my triggers have the same name on the oscillogram (S1). How can I change it to distinguish between audio beep and visual flash image stimuli?

Nevemind I figured it out by just needed to change the 0x01 to 0x02! Thanks for your help Michael you’ve been extremely helpful!

At the moment you are sending port.write([0x01]) for each trigger. You just need to send different values for each type of stimulus, depending on what values your EEG system will distinguish.

@Michael
Hi Michael, similar to @Achilles I want to add EEG triggers on the brainvision recording system using a serial port. I used the code that you have suggested in this message series. Unfortunately, nothing appeared in the EEG recording system. However, the print commands that I put for debugging exactly after port.write([0x01]) work. So it seems that port.write([0x01]) is not doing anything as when I wrote port.setDTR(True) I see a trigger named S12 on my EEG trace. Do you know anything why this happens? Thank you in advance