Sending triggers via lab streaming layer (LSL)

If this template helps then use it. If not then just delete and start from scratch.

OS (e.g. Win10): WIN11
PsychoPy version: 2025.1.1:
Standard Standalone Installation? yes
Do you want it to also run online? no
What are you trying to achieve?:

I want to send triggers (video_on, video_off) from PsychoPy (builder) to an EOG recording software via lab streaming layer (turned on in the EOG recording software).

What did you try to make it work?:

Tried a solution proposed by gemini:

Begin experiment tab in a code component before video routine (e.g. instructions routine)

from pylsl import StreamInfo, StreamOutlet

info = StreamInfo(name='PsychoPy_Markers', type='Markers', channel_count=1, nominal_srate=0, channel_format='string', source_id='PsychoPy_LSL_Source') 

global outlet 

outlet = StreamOutlet(info)

In the video routine in the beginn routine tab of a code component

if 'outlet' in globals(): 

outlet.push_sample(['video_on'])

In the end routine tab

if 'outlet' in globals(): 

outlet.push_sample(['video_off'])

What specifically went wrong when you tried that?:
PsychoPy breaks immediately after pressing the run button without the possibilty to check the error message.

Thank you for any help!

Dear M11,

Even if the experiment crashes, you should still get some output to the psychopy runner. Are you able to input participant ID?

How are you sending triggers to the EOG? Parallel, Serial, USB?

Have you made a path for the pylsl (streaminfo/streamoutlet)? AFAIK it’s not an included library (but maybe I am wrong). So you’d need to install that, then set the path for that in your settings. Adding external modules to Standalone PsychoPy — PsychoPy v2025.2.2

Issac

Dear Issac,

Thank you so much for your reply.

Yes, I can input participant ID but immediately afterwards I see a running circle with a lot of information in the psychopy runner window (in the screenshot below is the only information I can see).

But when I click into the window the runner and builder window gets milky and a message appears that Python is not reacting.

Regarding your questions:

I am sending the triggers via Bluetooth.

No I have not made a path, but based on your link I looked up whether pylsl is installed and yes, I can find a folder in PsychoPy > Lib > site-packages. As you recommended I tried to set the path in the settings but I can not find where (and how should the path look like?).

Firstly, it’s typically recommended to avoid the use of global and globals() unless absolutely necessary. And I don’t think it’s necessary here.

Basic setup for using LSL for event-marking in Builder

Ensure LSL (pylsl) is installed for PsychoPy e.g., via pip thorugh PsychoPy’s Plugins/packages manager.

In a single Code component*, located in the Routine associated with the event that is updated e.g., a trial/stimulus in a cognitive task from a conditions file or other dict/list:

Begin Experiment

import pylsl as LSL

uid4LSL = ‘7357’ # or some hard-coded value or e.g., expInfo[‘Participant’]
streamName = expInfo[‘expName’]  # or whatever you want it to be called
streamDataType = ‘string’  # or ‘int32’ if markers are only integers

stream = LSL.StreamInfo(streamName,‘Markers’,1,0,streamDataType,uid4LSL)
outlet = LSL.StreamOutlet(stream)

Begin Routine

# e.g., event_marker == stimulus code from conditions
if streamDataType == 'string' :
    LSL_event = str(event_marker)
elif streamDataType == 'int32' :
    LSL_event = int(event_marker)

win.callOnFlip(outlet.push_sample,[LSL_event])

End Routine

# optional
if streamDataType == 'string' :
    outlet.push_sample(['Fin'])
elif streamDataType == 'int32' :
    outlet.push_sample([0])

Optional but strongly recommended

Implement an LSL event-marker “ping” e.g., 1/s at the start of your experiment to check the markers are coming through to the receiving system before starting data collection.

Begin Experiment

f = 0
ping = '7357'  # string for string streams, int for int32 streams

Each Frame

f +=1
if f % 60 == 0 :
    outlet.push_sample([ping])

N.B. The if streamDataType statements are not necessary and can be removed if you just consistently code for using a string or int32 type stream.

*This code can be separated into multiple Code components and that might be preferable or even necessary for some setups, but sometimes keeping the code together in one related Code component can be easiest.

This is what we use all the time in our labs, but please let me know if there are any issues, typos, or just flat-out mistakes in the above code (as I have modified it slightly for sharing more widely).

Dear M11,

(*Note: I don’t know a whole lot about net problems so take this with a grain of salt) Looking at the error output it looks like you haven’t properly setup communications and port addresses with the bluetooth device. The errors are saying (based off my minimal understanding) that I can’t even start to talk to devices and find them properly.

One thing I would recommend to do is try to make a basic python script in an IDE to check if you are able to communicate with the device properly.

The other thing I’d recommend you trying is something I found when I had issues with a parallel port device.

“Now this is the key problem I think with Windows 11 that people may be having. Check if you have .Net Framework 3.5 (2.0 and 3.0) enabled on your windows machine. Windows 11 will sometimes have this option off by default. Go to the search bar, type “Turn windows features on or off”→ click to enable “.NET Framework 3.5 (2.0 and 3.0)”. This will most likely require a windows update to download and install the necessary files to enable it.”

Try doing that and see if that helps at all?

Also try the cleaner code snippets from @PsyTechMMU as well.

Issac

1 Like

Hi @M11

Just also sharing these slides from a recent workshop we ran that used OpenBCI/Emotibits/LSL in case they’re helpful! Slide 10 in particular might help.

1 Like

Dear PsyTechMMU
Dear Issac
Dear Kimberley_Dundas

Thank you so much for your helpful replies. They are absolutely gorgeous.
It took me a while to realise that the best approach is to integrate the PsychoPy markers and my EOG via LabRecorder: https://github.com/labstreaminglayer/App-LabRecorder.Now, markers are being sent without errors, and it seems that at least a small testing experiment is working. Thank you again to everyone who helped! I also found this source useful: GitHub - kaczmarj/psychopy-lsl: Use LabStreamingLayer to handle triggers with PsychoPy.

Thanks for all of the great info. I have been trying to use a muse2 to collect EEG data and sending a stream from Bluemuse for the ERG data and a second stream from psychopy for the markers. Here is my code from Begin experiment:

#In the “Begin Experiment” tab

from pylsl import StreamInlet, resolve_byprop
import numpy as np

#A function to find the LSL streams

def find_muse_stream():
streams = resolve_byprop(‘type’, ‘EEG’, timeout=2)
if streams:
return streams[0]
return None

#A function to push markers to LSL

def push_marker(outlet, marker):
outlet.push_sample(np.array([marker]))

#Try to find and set up the BlueMuse stream

print(“Looking for Muse LSL stream…”)
stream_info = find_muse_stream()
if stream_info is None:
print(“Could not find Muse LSL stream. Please ensure BlueMuse is running.”)
inlet = None
else:
inlet = StreamInlet(stream_info, max_chunklen=12)
print(“Found Muse LSL stream.”)

#Check if the marker outlet is available. If not, create one.

#try:
marker_outlet
except NameError:
from pylsl import StreamInfo, StreamOutlet
info = StreamInfo(‘PsychoPy_Markers’, ‘Markers’, 1, 0, ‘string’, ‘myuniquesourceid’)
marker_outlet = StreamOutlet(info)

my code for begin routine:

#In the “Begin Routine” tab

#This code sends a marker when the trial begins

if marker_outlet:
trial_start_marker = f"Trial_{t:.2f}"
push_marker(marker_outlet, trial_start_marker)
print(f"Sent marker: {trial_start_marker}")

and my code from end routine:

#In the “End Routine” tab

#This code sends a marker when the routine ends

if marker_outlet:
trial_end_marker = f"End_Trial_{t:.2f}"
push_marker(marker_outlet, trial_end_marker)
print(f"Sent marker: {trial_end_marker}")

The resulting xdf file has two streams, I can see both. However, when I go to import these into EEGLab, the xdf-import tool says the file does not exist. I have tried ERPstudio and it will import just the EEG stream but nothing else (no markers). Finally, I have tried mobilab, but there I get a recursive error. I need to merge the streams, but I just cannot figure out how to do that.

Thanks