Sending a trigger via serial port (Biosemi) synchronized on keyboard press (USB HID library)

Hi community,
I recently acquired a simple button box (blackbox toolkit, 1000hz sampling) that can be easily interfaced using the new psychopy’s keyboard component linked to the USB HID library. The button box contains two buttons, detected as ‘1’ and ‘2’ by keyboard.getKeys() respectively. So far, so good.
Here is the issue. I am planning to record the EMG activity of response muscles in a random dot motion experiment, so I need to send a trigger synchronized to the button press. Of course I could do that in my psychopy loop (see code below), but I would loose some temporal resolution (the trigger would be tethered to the refresh rate, and would not reflect the time at which the button was pressed). My question is thus very simple, but probably hard to solve. Is there a way I could easily modify the code below to send a trigger synchronized with the button press ? Of course I could use a thread to monitor my button box, or the iohub module, but I’d prefer using the (much simpler) keyboard component.
All the best

from psychopy import core, data, event, visual, gui
import numpy as np
import os
from pathlib import Path
from psychopy.hardware import keyboard
import serial

port = serial.Serial("COM3",baudrate =115200)


#######################Parameters for RDK stimuli
framerate = 60.0
speed_deg_sec =8.#8.0 #in degrees/s 12 in Pilly in Seitz
dot_density = 16.7#in deg-2 s-1
rayon_cercle = 9.0#in deg
number_of_dots = int(np.round(dot_density * np.pi * np.square(rayon_cercle) * (1/framerate)))
#######################

expName="RDK_SAT_task"
expInfo={'participant':''}

expName='RDK_task'
expInfo={'participant':''}
dlg=gui.DlgFromDict(dictionary=expInfo,title=expName)
if dlg.OK==False: core.quit() #user pressed cancel

win = visual.Window(fullscr=True, monitor='mathieu',size=(1920, 1080), color=[0,0,0], colorSpace='rgb255', units='deg')
police = 'Consolas'
win.setMouseVisible(False)

# initialize keyboards
resp = keyboard.Keyboard()
defaultKeyboard = keyboard.Keyboard()#for escaping only


dots=visual.DotStim(win=win, name='random_dots',
    nDots= number_of_dots, 
    dotSize=4,
    units = 'deg',
    speed= speed_deg_sec/framerate,
    dir= 0, 
    coherence= 0.0,
    fieldPos=[0.0, 0.0],
    fieldSize=rayon_cercle*2,
    fieldShape='circle',
    signalDots='different', 
    noiseDots='position', 
    dotLife= -1,
    color=[255,255,255], colorSpace='rgb255')

kb_delay=0
dots.setFieldCoherence(0.2)
dots.setDir(0)#0 (right) or 180 (left)   


win.callOnFlip(resp.clock.reset)
win.callOnFlip(port.write,b'1')#trigger for stimulus onset
win.callOnFlip(resp.clearEvents, eventType='keyboard')
while True:   

    if kb_delay==1:#check keyboard buffer AFTER first draw of stimulus and clock reset        
        theseKeys = resp.getKeys(keyList=['1','2'], waitRelease=False)
        if len(theseKeys):
            theseKeys = theseKeys[0] 
            if theseKeys.name == '1':#left button press
                port.write(b'2')#trigger for left response
            if theseKeys.name == '2':#right button press
                port.write(b'3')#trigger for right response
            break
        if defaultKeyboard.getKeys(keyList=["escape"], waitRelease=False):
            core.quit()              
    
    dots.draw()
    win.flip()  
    if kb_delay == 0:
        kb_delay = 1      
core.quit()

I am bumping this question because I am encountering a similar issue. I need to send TTL pulse to EEG amplifier to mark the onset of button press with high accuracy. I am using Builder.

I’ve spent a couple hours digging through the docs, and discussions here and on the google group. I still have some confusion & questions. If you know the answer to any of these please chime in.

  • Is sending triggers faster than the screen refresh rate supported in builder? Or in builder + code components? Or in coder?

  • Based on old posts (e.g., Re: Using Eyelink 1000 parallel port), it would seem possible to use iohub module for this purpose. I’ve looked through the iohub docs but am still not sure about the best-practice way to implement this. Is there a worked example, or class/method that someone could point me to, in iohub that could be used for this? Or in any other module (e.g. psychtoolbox) that could be used for this purpose?

I have been enjoying Psychopy so far, but admittedly am concerned about this issue as it seems to be a rather serious limitation for response-locked EEG analyses like mine. Thanks!

Synchronization is the bane of an EEG researcher’s life. What button box and EEG system are you using?

The “easy” solution that I use often with EyeLink and EEG amplifiers (EGI and BrainProducts) is to split the button press into two streams, with one going to the PsychoPy computer and the other going to the TTL input on the EEG amplifier. You then get a TTL in the EEG stream and can separately use the input response (if necessary) in PsychoPy for doing logic conditionals.

The more challenging one is using ioHub where you’d need to get the time-stamp (easy) and then communicate that to your EEG amplifier in a way that the EEG amplifier can handle it (more challenging).

1 Like

@pmolfese
We’re using BrainProducts actiCHamp, with a trigger box and two pyka 5 button handhelds, fiber optics with USB connections. But we could swap these response boxes out with others.

to split the button press into two streams

Oh, interesting! I haven’t seriously considered this idea. What sort of equipment do you use for this?

Not explicitly - as Code Components work by inserting code within the frame loop. To do stuff outside of the frame loop, your best options are either threading or (as @pmolfese suggests) outsourcing the processing to a piece of hardware.

You can, however, initialise a thread via a Code Component in a Builder experiment! I would probably do it something like this in the Begin Experiment or Begin Routine tab (choosing those tabs as it needs to happen after the Keyboard and Serial objects have been created):

import time
import threading


# define a function which is run continuously by a thread
def pollKeyboard(kb, port):
    """
    Continuously poll a keyboard for responses, and execute some code when a particular response is 
    received.

    Parameters
    ----------
    kb : psychopy.hardware.keyboard.Keyboard
        Keyboard Component to poll
    port : serial.Serial
        Serial port to send a signal to on press
    """
    # run forever (only ever do this inside a daemonic thread!)
    while True:
        # poll the keyboard for the desired keypress events
        presses = kb.getKeys(keyList=['1','2'], waitRelease=False)
        # if we got them, perform the function *now*
        if presses:
            # whatever you want to happen when a key is pressed (I've just copied from OP)
            if presses[0].name == '1':
                port.write(b'2')
            if presses[0].name == '2':
                port.write(b'3')
        # sleep for 1ms so that other threads can do stuff
        time.sleep(0.001)

# create a thread to run this function continuously alongside the experiment
pollingThread = threading.Thread(
    target=pollKeyboard,
    kwargs={
        'kb': resp, # your Keyboard Component
        'port': port, # your serial port
    },
    daemon=True,  # a "daemonic" thread is one which terminates when the experiment finishes, without daemon=True it would seize up after finishing!
)
# start the thread
pollingThread.start()
1 Like

Thanks @TParsons! That’s a very helpful pointer and code snippets. I am now looking into the threading module.

I’m trying to understand how threading like this would impact clocks/timing of processing in the main psychopy thread. Is the idea that pollKeyboard() would run so quickly that most of the wall time would actually be spent in time.sleep(0.001), and so have negligible impact?

It shouldn’t affect clocks as they aren’t continuously doing anything - they just take the time and subtract it from their start time when accessed. As for other processes, yes that’s essentially it! Python will allocate resources between threads as needed, so if one thread is sleeping then it allows the others (i.e. your experiment) to happen. getKeys isn’t a particularly costly call so I think 1ms should be enough, but if you notice any lag in the rest of the experiment you can always increase that sleep time. Just keep in mind that the sleep time is essentially the temporal resolution of your keypress sampling - if you’re sleeping for 2ms then you’ll only get keypresses at the next 2ms interval.

Apologies for the delay, our cable didn’t have a label on it so I was trying to figure out if someone made it here in our shop. The Current Designs folks were kind enough to remind me of their two handy products sending TTLs from the 932 box:

  1. Fiber Optic Response Devices Cable - 9 TTL Outputs for the 932
  2. Fiber Optic Response Devices 932 Interface Parallel Cable

If you go the hardware route I hope that this helps!