digital-trigger 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digital_trigger-0.1.2/PKG-INFO +176 -0
- digital_trigger-0.1.2/README.md +153 -0
- digital_trigger-0.1.2/pyproject.toml +38 -0
- digital_trigger-0.1.2/setup.cfg +4 -0
- digital_trigger-0.1.2/src/digital_trigger/__init__.py +3 -0
- digital_trigger-0.1.2/src/digital_trigger/trigger.py +129 -0
- digital_trigger-0.1.2/src/digital_trigger.egg-info/PKG-INFO +176 -0
- digital_trigger-0.1.2/src/digital_trigger.egg-info/SOURCES.txt +9 -0
- digital_trigger-0.1.2/src/digital_trigger.egg-info/dependency_links.txt +1 -0
- digital_trigger-0.1.2/src/digital_trigger.egg-info/requires.txt +1 -0
- digital_trigger-0.1.2/src/digital_trigger.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: digital-trigger
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Simple class for simplified pySerial interface for use in TTL event marking using e.g. PsychoPy.
|
|
5
|
+
Author-email: Max Lovell <max_lovell@hotmail.co.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Max-Lovell/triggers
|
|
8
|
+
Project-URL: Issues, https://github.com/Max-Lovell/triggers/issues
|
|
9
|
+
Keywords: psychology,neuroscience,psychopy,eeg,triggers,serial
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pyserial>=3.0
|
|
23
|
+
|
|
24
|
+
# digital-trigger
|
|
25
|
+
|
|
26
|
+
Easier sending of digital triggers/event markers/TTL signals using the Serial (pySerial) package with a Black Box Toolkit USB TTL module in Python and PsychoPy.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install digital_trigger
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.8+ and [`pyserial`](https://pypi.org/project/pyserial/) (installed automatically).
|
|
35
|
+
|
|
36
|
+
In PsychoPy you can install packages in the Builder GUI by going to:
|
|
37
|
+
Tools > Plugins and packages manager > Packages > Open PIP terminal, and run `pip install digital-trigger`
|
|
38
|
+
|
|
39
|
+
To find COM port number:
|
|
40
|
+
- run the command `python -m serial.tools.list_ports -v`
|
|
41
|
+
- On Windows, open Device Manager, expand "Ports (COM & LPT)", unplug and replug to see which is your device
|
|
42
|
+
- On Mac run `ls /dev/cu.*` in Terminal and look for something like /dev/cu.usbserial-XXXX or /dev/cu.usbmodemXXXX — use the cu.* name, not tty.*.
|
|
43
|
+
- On Linux, run ls `/dev/ttyUSB* /dev/ttyACM*` — USB-serial adapters are usually `ttyUSB0`
|
|
44
|
+
- Arduino-style boards `ttyACM0; dmesg | tail` right after plugging in shows the assigned name, and you may need to add yourself to the dialout group for permission.
|
|
45
|
+
|
|
46
|
+
Make sure your COM port is set up with a latency of 1ms (or lower) and Baudrate of 115200. This can be done under Device Manager > COM port > Advanced on Windows.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### PsychoPy
|
|
51
|
+
|
|
52
|
+
Add the following in a 'custom code block' in the routine where your stimuli are presented.
|
|
53
|
+
Note drag the Code Component so it sits below the stimulus component in the routine view, as PsychoPy runs them top to bottom.
|
|
54
|
+
Consider managing the triggers using a 'trigger_line' variable in your conditions file.
|
|
55
|
+
|
|
56
|
+
#### Before Experiment
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
from digital_trigger import Trigger
|
|
60
|
+
port = Trigger('COM4', names=['cond_1', 'cond_2', 'stim_1', 'stim_2'])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Note you can only do this once per experiment.
|
|
64
|
+
Don't do this in multiple code blocks in different routines or you will get a 'access/permission denied' error.
|
|
65
|
+
Probably best to have a single code block jsut for this in your first routine even.
|
|
66
|
+
|
|
67
|
+
#### Begin Routine
|
|
68
|
+
```
|
|
69
|
+
trigger_opened = False
|
|
70
|
+
trigger_closed = False
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Each Frame
|
|
74
|
+
```
|
|
75
|
+
# Open the line the instant the stimulus is drawn to the screen...
|
|
76
|
+
# Change 'image' to the name of your stimulus component
|
|
77
|
+
if image.status == STARTED and not trigger_opened:
|
|
78
|
+
win.callOnFlip(port.open, 'stim_1')
|
|
79
|
+
trigger_opened = True
|
|
80
|
+
|
|
81
|
+
# ...and close it the instant the stimulus is removed.
|
|
82
|
+
if image.status == FINISHED and not trigger_closed:
|
|
83
|
+
win.callOnFlip(port.close, 'stim_1')
|
|
84
|
+
trigger_closed = True
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### End Routine
|
|
88
|
+
```
|
|
89
|
+
if not trigger_closed:
|
|
90
|
+
port.close('stim_1')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### End Experiment
|
|
94
|
+
```
|
|
95
|
+
port.stop()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Python
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from digital_trigger import Trigger
|
|
102
|
+
|
|
103
|
+
port = Trigger('COM4', names=['cond_1', 'cond_2', 'stim_1', 'stim_2'])
|
|
104
|
+
|
|
105
|
+
port.open('stim_1') # turn a line on (by name)
|
|
106
|
+
port.open([1, 'cond_2']) # turn several on (numbers and names mixed)
|
|
107
|
+
port.close('stim_1') # turn one off
|
|
108
|
+
port.close_all() # turn everything off
|
|
109
|
+
port.open_lines() # index of open lines and prints the bitmask
|
|
110
|
+
port.status() # list of bools for each port if open or closed
|
|
111
|
+
|
|
112
|
+
port.stop() # reset all lines and close the port
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or as a context manager, which closes the port for you:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
with Trigger('COM4', names=['stim_1']) as port:
|
|
119
|
+
port.open('stim_1')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Resources:
|
|
123
|
+
- https://www.blackboxtoolkit.com/support_usb_ttl_module.html
|
|
124
|
+
- https://www.blackboxtoolkit.com/docs/pdf/USBTTLv1r19.pdf
|
|
125
|
+
- https://psychopy.org/developers/pluginDevGuide.html#plugindevguide
|
|
126
|
+
|
|
127
|
+
# Dev notes
|
|
128
|
+
|
|
129
|
+
#### build & upload
|
|
130
|
+
Setup venv:
|
|
131
|
+
```
|
|
132
|
+
python3 -m venv .venv
|
|
133
|
+
source .venv/bin/activate
|
|
134
|
+
pip install --upgrade pip build twine pytest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
re-run build:
|
|
138
|
+
```
|
|
139
|
+
rm -rf dist # clear old builds so nothing stale is uploaded
|
|
140
|
+
python3 -m build
|
|
141
|
+
ls dist # confirm the new version number is shown
|
|
142
|
+
twine upload --repository testpypi dist/*
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### test
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
cd /tmp
|
|
149
|
+
rm -rf testenv
|
|
150
|
+
python3 -m venv testenv
|
|
151
|
+
source testenv/bin/activate
|
|
152
|
+
pip install -i https://test.pypi.org/simple/ \
|
|
153
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
154
|
+
digital-trigger==0.1.2 # match the version
|
|
155
|
+
python -c "from digital_trigger import Trigger; t = Trigger(simulate=True, names=['stim_1']); t.open('stim_1'); print('ok', t.bitmask)"
|
|
156
|
+
deactivate
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To install from the test repo on a new computer:
|
|
160
|
+
```
|
|
161
|
+
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ digital-trigger==0.1.2
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Small script to check:
|
|
165
|
+
```
|
|
166
|
+
from digital_trigger import Trigger
|
|
167
|
+
t = Trigger(simulate=True, names=['stim_1'])
|
|
168
|
+
t.open('stim_1')
|
|
169
|
+
print('ok', t.bitmask)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### release
|
|
173
|
+
```
|
|
174
|
+
cd /path/to/project # back to the project folder
|
|
175
|
+
twine upload dist/* # no --repository = real PyPI
|
|
176
|
+
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# digital-trigger
|
|
2
|
+
|
|
3
|
+
Easier sending of digital triggers/event markers/TTL signals using the Serial (pySerial) package with a Black Box Toolkit USB TTL module in Python and PsychoPy.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install digital_trigger
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.8+ and [`pyserial`](https://pypi.org/project/pyserial/) (installed automatically).
|
|
12
|
+
|
|
13
|
+
In PsychoPy you can install packages in the Builder GUI by going to:
|
|
14
|
+
Tools > Plugins and packages manager > Packages > Open PIP terminal, and run `pip install digital-trigger`
|
|
15
|
+
|
|
16
|
+
To find COM port number:
|
|
17
|
+
- run the command `python -m serial.tools.list_ports -v`
|
|
18
|
+
- On Windows, open Device Manager, expand "Ports (COM & LPT)", unplug and replug to see which is your device
|
|
19
|
+
- On Mac run `ls /dev/cu.*` in Terminal and look for something like /dev/cu.usbserial-XXXX or /dev/cu.usbmodemXXXX — use the cu.* name, not tty.*.
|
|
20
|
+
- On Linux, run ls `/dev/ttyUSB* /dev/ttyACM*` — USB-serial adapters are usually `ttyUSB0`
|
|
21
|
+
- Arduino-style boards `ttyACM0; dmesg | tail` right after plugging in shows the assigned name, and you may need to add yourself to the dialout group for permission.
|
|
22
|
+
|
|
23
|
+
Make sure your COM port is set up with a latency of 1ms (or lower) and Baudrate of 115200. This can be done under Device Manager > COM port > Advanced on Windows.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### PsychoPy
|
|
28
|
+
|
|
29
|
+
Add the following in a 'custom code block' in the routine where your stimuli are presented.
|
|
30
|
+
Note drag the Code Component so it sits below the stimulus component in the routine view, as PsychoPy runs them top to bottom.
|
|
31
|
+
Consider managing the triggers using a 'trigger_line' variable in your conditions file.
|
|
32
|
+
|
|
33
|
+
#### Before Experiment
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
from digital_trigger import Trigger
|
|
37
|
+
port = Trigger('COM4', names=['cond_1', 'cond_2', 'stim_1', 'stim_2'])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Note you can only do this once per experiment.
|
|
41
|
+
Don't do this in multiple code blocks in different routines or you will get a 'access/permission denied' error.
|
|
42
|
+
Probably best to have a single code block jsut for this in your first routine even.
|
|
43
|
+
|
|
44
|
+
#### Begin Routine
|
|
45
|
+
```
|
|
46
|
+
trigger_opened = False
|
|
47
|
+
trigger_closed = False
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Each Frame
|
|
51
|
+
```
|
|
52
|
+
# Open the line the instant the stimulus is drawn to the screen...
|
|
53
|
+
# Change 'image' to the name of your stimulus component
|
|
54
|
+
if image.status == STARTED and not trigger_opened:
|
|
55
|
+
win.callOnFlip(port.open, 'stim_1')
|
|
56
|
+
trigger_opened = True
|
|
57
|
+
|
|
58
|
+
# ...and close it the instant the stimulus is removed.
|
|
59
|
+
if image.status == FINISHED and not trigger_closed:
|
|
60
|
+
win.callOnFlip(port.close, 'stim_1')
|
|
61
|
+
trigger_closed = True
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### End Routine
|
|
65
|
+
```
|
|
66
|
+
if not trigger_closed:
|
|
67
|
+
port.close('stim_1')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### End Experiment
|
|
71
|
+
```
|
|
72
|
+
port.stop()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Python
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from digital_trigger import Trigger
|
|
79
|
+
|
|
80
|
+
port = Trigger('COM4', names=['cond_1', 'cond_2', 'stim_1', 'stim_2'])
|
|
81
|
+
|
|
82
|
+
port.open('stim_1') # turn a line on (by name)
|
|
83
|
+
port.open([1, 'cond_2']) # turn several on (numbers and names mixed)
|
|
84
|
+
port.close('stim_1') # turn one off
|
|
85
|
+
port.close_all() # turn everything off
|
|
86
|
+
port.open_lines() # index of open lines and prints the bitmask
|
|
87
|
+
port.status() # list of bools for each port if open or closed
|
|
88
|
+
|
|
89
|
+
port.stop() # reset all lines and close the port
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or as a context manager, which closes the port for you:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
with Trigger('COM4', names=['stim_1']) as port:
|
|
96
|
+
port.open('stim_1')
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Resources:
|
|
100
|
+
- https://www.blackboxtoolkit.com/support_usb_ttl_module.html
|
|
101
|
+
- https://www.blackboxtoolkit.com/docs/pdf/USBTTLv1r19.pdf
|
|
102
|
+
- https://psychopy.org/developers/pluginDevGuide.html#plugindevguide
|
|
103
|
+
|
|
104
|
+
# Dev notes
|
|
105
|
+
|
|
106
|
+
#### build & upload
|
|
107
|
+
Setup venv:
|
|
108
|
+
```
|
|
109
|
+
python3 -m venv .venv
|
|
110
|
+
source .venv/bin/activate
|
|
111
|
+
pip install --upgrade pip build twine pytest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
re-run build:
|
|
115
|
+
```
|
|
116
|
+
rm -rf dist # clear old builds so nothing stale is uploaded
|
|
117
|
+
python3 -m build
|
|
118
|
+
ls dist # confirm the new version number is shown
|
|
119
|
+
twine upload --repository testpypi dist/*
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### test
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
cd /tmp
|
|
126
|
+
rm -rf testenv
|
|
127
|
+
python3 -m venv testenv
|
|
128
|
+
source testenv/bin/activate
|
|
129
|
+
pip install -i https://test.pypi.org/simple/ \
|
|
130
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
131
|
+
digital-trigger==0.1.2 # match the version
|
|
132
|
+
python -c "from digital_trigger import Trigger; t = Trigger(simulate=True, names=['stim_1']); t.open('stim_1'); print('ok', t.bitmask)"
|
|
133
|
+
deactivate
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
To install from the test repo on a new computer:
|
|
137
|
+
```
|
|
138
|
+
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ digital-trigger==0.1.2
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Small script to check:
|
|
142
|
+
```
|
|
143
|
+
from digital_trigger import Trigger
|
|
144
|
+
t = Trigger(simulate=True, names=['stim_1'])
|
|
145
|
+
t.open('stim_1')
|
|
146
|
+
print('ok', t.bitmask)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### release
|
|
150
|
+
```
|
|
151
|
+
cd /path/to/project # back to the project folder
|
|
152
|
+
twine upload dist/* # no --repository = real PyPI
|
|
153
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "digital-trigger"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Simple class for simplified pySerial interface for use in TTL event marking using e.g. PsychoPy."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Max Lovell", email = "max_lovell@hotmail.co.uk" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["psychology", "neuroscience", "psychopy", "eeg", "triggers", "serial"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Scientific/Engineering",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pyserial>=3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Max-Lovell/triggers"
|
|
35
|
+
Issues = "https://github.com/Max-Lovell/triggers/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import serial
|
|
2
|
+
|
|
3
|
+
class Trigger:
|
|
4
|
+
def __init__(self, port='COM3', baudrate=115200, timeout=0, names=None, simulate=False):
|
|
5
|
+
self.bitmask = 0
|
|
6
|
+
self.names = names
|
|
7
|
+
self.simulate = simulate
|
|
8
|
+
if not simulate:
|
|
9
|
+
self.port = serial.Serial(port, baudrate, timeout=timeout)
|
|
10
|
+
self.reset()
|
|
11
|
+
self.write()
|
|
12
|
+
|
|
13
|
+
# -- line number handling ------------------------------------------
|
|
14
|
+
|
|
15
|
+
def name2line(self, name):
|
|
16
|
+
if self.names is None:
|
|
17
|
+
raise ValueError("No line names were set. Pass names=[...] to Trigger(), or use line numbers.")
|
|
18
|
+
return self.names.index(name) + 1
|
|
19
|
+
|
|
20
|
+
def get_line_numbers(self, lines):
|
|
21
|
+
# Wrap a single item so everything below can assume an iterable.
|
|
22
|
+
if isinstance(lines, (str, int)):
|
|
23
|
+
lines = [lines]
|
|
24
|
+
|
|
25
|
+
numbers = []
|
|
26
|
+
for line in lines:
|
|
27
|
+
if isinstance(line, str):
|
|
28
|
+
line = self.name2line(line)
|
|
29
|
+
else:
|
|
30
|
+
line = int(line)
|
|
31
|
+
if not 1 <= line <= 8:
|
|
32
|
+
raise ValueError("Line number must be between 1 and 8")
|
|
33
|
+
numbers.append(line)
|
|
34
|
+
return numbers
|
|
35
|
+
|
|
36
|
+
# -- trigger control -------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def open(self, line):
|
|
39
|
+
for l in self.get_line_numbers(line):
|
|
40
|
+
self.bitmask |= (1 << (l - 1))
|
|
41
|
+
self.write()
|
|
42
|
+
|
|
43
|
+
def close(self, line):
|
|
44
|
+
for l in self.get_line_numbers(line):
|
|
45
|
+
self.bitmask &= ~(1 << (l - 1))
|
|
46
|
+
# self.port.flush() # blocks until byte is sent - not necessary
|
|
47
|
+
self.write()
|
|
48
|
+
|
|
49
|
+
def close_all(self):
|
|
50
|
+
self.bitmask = 0
|
|
51
|
+
self.write()
|
|
52
|
+
|
|
53
|
+
# -- serial I/O ------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def reset(self):
|
|
56
|
+
self.bitmask = 0
|
|
57
|
+
if not self.simulate:
|
|
58
|
+
# b'' is identical to ''.encode() for something this simple.
|
|
59
|
+
self.port.write(b'RR') # 'RR' is a device-specific command to reset.
|
|
60
|
+
else:
|
|
61
|
+
print('Port reset')
|
|
62
|
+
|
|
63
|
+
def write(self):
|
|
64
|
+
# See documentation at the bottom here: https://www.blackboxtoolkit.com/support_usb_ttl_module.html
|
|
65
|
+
payload = format(self.bitmask, '02X')
|
|
66
|
+
if not self.simulate:
|
|
67
|
+
self.port.write(payload.encode())
|
|
68
|
+
else:
|
|
69
|
+
print('Hex code written: ' + payload)
|
|
70
|
+
# self.open_lines()
|
|
71
|
+
|
|
72
|
+
def stop(self):
|
|
73
|
+
print('Shutting down port')
|
|
74
|
+
if not self.simulate:
|
|
75
|
+
self.reset()
|
|
76
|
+
self.port.close()
|
|
77
|
+
print('Port is closed: ', not self.port.is_open)
|
|
78
|
+
|
|
79
|
+
# -- OPTIONAL EXTRAS -------------------------------------------------
|
|
80
|
+
# -- display -------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def is_open(self, lines):
|
|
83
|
+
return all(self.status(lines))
|
|
84
|
+
|
|
85
|
+
def is_closed(self, lines):
|
|
86
|
+
return not any(self.status(lines))
|
|
87
|
+
|
|
88
|
+
def status(self, lines=None):
|
|
89
|
+
if lines is None:
|
|
90
|
+
lines = range(1, 9)
|
|
91
|
+
|
|
92
|
+
matches = []
|
|
93
|
+
for l in self.get_line_numbers(lines): # simplify
|
|
94
|
+
is_open = self.bitmask & (1 << (l - 1))
|
|
95
|
+
matches.append(bool(is_open)) # Unsure if should convert to bool or not?
|
|
96
|
+
# print(f"Line {str(l)} is {'open' if is_open else 'closed'} {self.line2name(l)}")
|
|
97
|
+
return matches
|
|
98
|
+
|
|
99
|
+
def open_lines(self):
|
|
100
|
+
lines = []
|
|
101
|
+
for l in range(8):
|
|
102
|
+
if self.bitmask & (1 << l):
|
|
103
|
+
lines.append(l + 1)
|
|
104
|
+
print('Open lines:', lines, "{:08b}".format(self.bitmask))
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
def line2name(self, line):
|
|
108
|
+
if self.names is None:
|
|
109
|
+
return []
|
|
110
|
+
if isinstance(line, int):
|
|
111
|
+
line = [line]
|
|
112
|
+
|
|
113
|
+
line_names = []
|
|
114
|
+
for l in line:
|
|
115
|
+
l = int(l) #just incase it's a numpy int
|
|
116
|
+
if 1 <= l <= len(self.names):
|
|
117
|
+
line_names.append(self.names[l - 1])
|
|
118
|
+
else:
|
|
119
|
+
line_names.append('unnamed')
|
|
120
|
+
return line_names
|
|
121
|
+
|
|
122
|
+
# -- context manager support ----------------------------------------
|
|
123
|
+
|
|
124
|
+
def __enter__(self):
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
128
|
+
self.stop()
|
|
129
|
+
return False
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: digital-trigger
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Simple class for simplified pySerial interface for use in TTL event marking using e.g. PsychoPy.
|
|
5
|
+
Author-email: Max Lovell <max_lovell@hotmail.co.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Max-Lovell/triggers
|
|
8
|
+
Project-URL: Issues, https://github.com/Max-Lovell/triggers/issues
|
|
9
|
+
Keywords: psychology,neuroscience,psychopy,eeg,triggers,serial
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pyserial>=3.0
|
|
23
|
+
|
|
24
|
+
# digital-trigger
|
|
25
|
+
|
|
26
|
+
Easier sending of digital triggers/event markers/TTL signals using the Serial (pySerial) package with a Black Box Toolkit USB TTL module in Python and PsychoPy.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install digital_trigger
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.8+ and [`pyserial`](https://pypi.org/project/pyserial/) (installed automatically).
|
|
35
|
+
|
|
36
|
+
In PsychoPy you can install packages in the Builder GUI by going to:
|
|
37
|
+
Tools > Plugins and packages manager > Packages > Open PIP terminal, and run `pip install digital-trigger`
|
|
38
|
+
|
|
39
|
+
To find COM port number:
|
|
40
|
+
- run the command `python -m serial.tools.list_ports -v`
|
|
41
|
+
- On Windows, open Device Manager, expand "Ports (COM & LPT)", unplug and replug to see which is your device
|
|
42
|
+
- On Mac run `ls /dev/cu.*` in Terminal and look for something like /dev/cu.usbserial-XXXX or /dev/cu.usbmodemXXXX — use the cu.* name, not tty.*.
|
|
43
|
+
- On Linux, run ls `/dev/ttyUSB* /dev/ttyACM*` — USB-serial adapters are usually `ttyUSB0`
|
|
44
|
+
- Arduino-style boards `ttyACM0; dmesg | tail` right after plugging in shows the assigned name, and you may need to add yourself to the dialout group for permission.
|
|
45
|
+
|
|
46
|
+
Make sure your COM port is set up with a latency of 1ms (or lower) and Baudrate of 115200. This can be done under Device Manager > COM port > Advanced on Windows.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### PsychoPy
|
|
51
|
+
|
|
52
|
+
Add the following in a 'custom code block' in the routine where your stimuli are presented.
|
|
53
|
+
Note drag the Code Component so it sits below the stimulus component in the routine view, as PsychoPy runs them top to bottom.
|
|
54
|
+
Consider managing the triggers using a 'trigger_line' variable in your conditions file.
|
|
55
|
+
|
|
56
|
+
#### Before Experiment
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
from digital_trigger import Trigger
|
|
60
|
+
port = Trigger('COM4', names=['cond_1', 'cond_2', 'stim_1', 'stim_2'])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Note you can only do this once per experiment.
|
|
64
|
+
Don't do this in multiple code blocks in different routines or you will get a 'access/permission denied' error.
|
|
65
|
+
Probably best to have a single code block jsut for this in your first routine even.
|
|
66
|
+
|
|
67
|
+
#### Begin Routine
|
|
68
|
+
```
|
|
69
|
+
trigger_opened = False
|
|
70
|
+
trigger_closed = False
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Each Frame
|
|
74
|
+
```
|
|
75
|
+
# Open the line the instant the stimulus is drawn to the screen...
|
|
76
|
+
# Change 'image' to the name of your stimulus component
|
|
77
|
+
if image.status == STARTED and not trigger_opened:
|
|
78
|
+
win.callOnFlip(port.open, 'stim_1')
|
|
79
|
+
trigger_opened = True
|
|
80
|
+
|
|
81
|
+
# ...and close it the instant the stimulus is removed.
|
|
82
|
+
if image.status == FINISHED and not trigger_closed:
|
|
83
|
+
win.callOnFlip(port.close, 'stim_1')
|
|
84
|
+
trigger_closed = True
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### End Routine
|
|
88
|
+
```
|
|
89
|
+
if not trigger_closed:
|
|
90
|
+
port.close('stim_1')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### End Experiment
|
|
94
|
+
```
|
|
95
|
+
port.stop()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Python
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from digital_trigger import Trigger
|
|
102
|
+
|
|
103
|
+
port = Trigger('COM4', names=['cond_1', 'cond_2', 'stim_1', 'stim_2'])
|
|
104
|
+
|
|
105
|
+
port.open('stim_1') # turn a line on (by name)
|
|
106
|
+
port.open([1, 'cond_2']) # turn several on (numbers and names mixed)
|
|
107
|
+
port.close('stim_1') # turn one off
|
|
108
|
+
port.close_all() # turn everything off
|
|
109
|
+
port.open_lines() # index of open lines and prints the bitmask
|
|
110
|
+
port.status() # list of bools for each port if open or closed
|
|
111
|
+
|
|
112
|
+
port.stop() # reset all lines and close the port
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or as a context manager, which closes the port for you:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
with Trigger('COM4', names=['stim_1']) as port:
|
|
119
|
+
port.open('stim_1')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Resources:
|
|
123
|
+
- https://www.blackboxtoolkit.com/support_usb_ttl_module.html
|
|
124
|
+
- https://www.blackboxtoolkit.com/docs/pdf/USBTTLv1r19.pdf
|
|
125
|
+
- https://psychopy.org/developers/pluginDevGuide.html#plugindevguide
|
|
126
|
+
|
|
127
|
+
# Dev notes
|
|
128
|
+
|
|
129
|
+
#### build & upload
|
|
130
|
+
Setup venv:
|
|
131
|
+
```
|
|
132
|
+
python3 -m venv .venv
|
|
133
|
+
source .venv/bin/activate
|
|
134
|
+
pip install --upgrade pip build twine pytest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
re-run build:
|
|
138
|
+
```
|
|
139
|
+
rm -rf dist # clear old builds so nothing stale is uploaded
|
|
140
|
+
python3 -m build
|
|
141
|
+
ls dist # confirm the new version number is shown
|
|
142
|
+
twine upload --repository testpypi dist/*
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### test
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
cd /tmp
|
|
149
|
+
rm -rf testenv
|
|
150
|
+
python3 -m venv testenv
|
|
151
|
+
source testenv/bin/activate
|
|
152
|
+
pip install -i https://test.pypi.org/simple/ \
|
|
153
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
154
|
+
digital-trigger==0.1.2 # match the version
|
|
155
|
+
python -c "from digital_trigger import Trigger; t = Trigger(simulate=True, names=['stim_1']); t.open('stim_1'); print('ok', t.bitmask)"
|
|
156
|
+
deactivate
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To install from the test repo on a new computer:
|
|
160
|
+
```
|
|
161
|
+
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ digital-trigger==0.1.2
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Small script to check:
|
|
165
|
+
```
|
|
166
|
+
from digital_trigger import Trigger
|
|
167
|
+
t = Trigger(simulate=True, names=['stim_1'])
|
|
168
|
+
t.open('stim_1')
|
|
169
|
+
print('ok', t.bitmask)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### release
|
|
173
|
+
```
|
|
174
|
+
cd /path/to/project # back to the project folder
|
|
175
|
+
twine upload dist/* # no --repository = real PyPI
|
|
176
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/digital_trigger/__init__.py
|
|
4
|
+
src/digital_trigger/trigger.py
|
|
5
|
+
src/digital_trigger.egg-info/PKG-INFO
|
|
6
|
+
src/digital_trigger.egg-info/SOURCES.txt
|
|
7
|
+
src/digital_trigger.egg-info/dependency_links.txt
|
|
8
|
+
src/digital_trigger.egg-info/requires.txt
|
|
9
|
+
src/digital_trigger.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyserial>=3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
digital_trigger
|