psychopy 2024.2.1__py3-none-any.whl → 2024.2.4__py3-none-any.whl
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.
Potentially problematic release.
This version of psychopy might be problematic. Click here for more details.
- psychopy/.DS_Store +0 -0
- psychopy/GIT_SHA +1 -1
- psychopy/VERSION +1 -1
- psychopy/__init__.py +10 -1
- psychopy/__init__.py.orig +65 -0
- psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
- psychopy/app/Resources/.DS_Store +0 -0
- psychopy/app/_psychopyApp.py +11 -3
- psychopy/app/appData.spec +1 -1
- psychopy/app/builder/builder.py +1 -1
- psychopy/app/builder/builder.py.orig +3932 -0
- psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
- psychopy/app/builder/dialogs/paramCtrls.py +1 -1
- psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
- psychopy/app/colorpicker/__init__.py.orig +411 -0
- psychopy/app/cortex.log +0 -0
- psychopy/app/jobs.py +8 -1
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
- psychopy/app/plugin_manager/dialog.py +9 -7
- psychopy/app/ribbon.py +2 -1
- psychopy/app/runner/runner.py +7 -5
- psychopy/clock.py +8 -4
- psychopy/core.py.orig +169 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
- psychopy/demos/builder/Tools/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
- psychopy/demos/coder/iohub/.iohpid +1 -0
- psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
- psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
- psychopy/experiment/_experiment.py.orig +1032 -0
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/_base.py +13 -4
- psychopy/experiment/components/_base.py.orig +823 -0
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py +10 -1
- psychopy/experiment/components/microphone/__init__.py.orig +490 -0
- psychopy/experiment/components/polygon/__init__.py +21 -22
- psychopy/experiment/components/settings/__init__.py +13 -14
- psychopy/experiment/components/settings/__init__.py.orig +1337 -0
- psychopy/experiment/components/textbox/__init__.py.orig +310 -0
- psychopy/experiment/components/webcam/.DS_Store +0 -0
- psychopy/experiment/components/webcam/light/.DS_Store +0 -0
- psychopy/experiment/flow.py +10 -8
- psychopy/experiment/loops.py.orig +829 -0
- psychopy/experiment/params.py +8 -3
- psychopy/experiment/params.py.orig +408 -0
- psychopy/experiment/routine.py.orig +503 -0
- psychopy/experiment/routines/_base.py +15 -6
- psychopy/experiment/routines/counterbalance/__init__.py +1 -0
- psychopy/gui/qtgui.py +14 -7
- psychopy/gui/util.py +10 -14
- psychopy/gui/wxgui.py +10 -4
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/brainproducts.py.orig +680 -0
- psychopy/hardware/iolab.py.orig +238 -0
- psychopy/hardware/manager.py +1 -1
- psychopy/hardware/photodiode.py +59 -27
- psychopy/hardware/serialport.py +51 -0
- psychopy/hardware/speaker.py +4 -4
- psychopy/iohub/datastore/__init__.py.orig +443 -0
- psychopy/iohub/datastore/util.py.orig +692 -0
- psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
- psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
- psychopy/preferences/.DS_Store +0 -0
- psychopy/projects/pavlovia.py +10 -3
- psychopy/projects/pavlovia.py.orig +1295 -0
- psychopy/sound/backend_ptb.py +22 -5
- psychopy/sound/transcribe.py +24 -4
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestCircle_fill_local.png +0 -0
- psychopy/tests/data/__test.png +0 -0
- psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
- psychopy/tests/data/aperture1_norm_local.png +0 -0
- psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_height_local.png +0 -0
- psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_norm_local.png +0 -0
- psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
- psychopy/tests/data/blend_add_height_local.png +0 -0
- psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
- psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
- psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
- psychopy/tests/data/blend_add_norm_local.png +0 -0
- psychopy/tests/data/blend_add_stencil_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
- psychopy/tests/data/circleHex_height_local.png +0 -0
- psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
- psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
- psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
- psychopy/tests/data/circleHex_norm_local.png +0 -0
- psychopy/tests/data/circleHex_stencil_local.png +0 -0
- psychopy/tests/data/color_comparison_local.png +0 -0
- psychopy/tests/data/corrFullRandom_local.csv +16 -0
- psychopy/tests/data/corrFullRandom_local.tsv +6 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/dots_height_local.png +0 -0
- psychopy/tests/data/dots_normAddBlend_local.png +0 -0
- psychopy/tests/data/dots_normHexbackground_local.png +0 -0
- psychopy/tests/data/dots_normNoShade_local.png +0 -0
- psychopy/tests/data/dots_norm_local.png +0 -0
- psychopy/tests/data/dots_stencil_local.png +0 -0
- psychopy/tests/data/elarray1_height_local.png +0 -0
- psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
- psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
- psychopy/tests/data/elarray1_norm_local.png +0 -0
- psychopy/tests/data/elarray1_stencil_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
- psychopy/tests/data/gabor1_height_local.png +0 -0
- psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
- psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
- psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
- psychopy/tests/data/gabor1_norm_local.png +0 -0
- psychopy/tests/data/gabor1_stencil_local.png +0 -0
- psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_height_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
- psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
- psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
- psychopy/tests/data/movFrame1_stencil_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/numpyImage_height_local.png +0 -0
- psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
- psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
- psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
- psychopy/tests/data/numpyImage_norm_local.png +0 -0
- psychopy/tests/data/numpyImage_stencil_local.png +0 -0
- psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
- psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
- psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
- psychopy/tests/data/shape2_1_norm_local.png +0 -0
- psychopy/tests/data/shape2_1_stencil_local.png +0 -0
- psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
- psychopy/tests/data/text1_height_local.png +0 -0
- psychopy/tests/data/text1_normAddBlend_local.png +0 -0
- psychopy/tests/data/text1_normHexbackground_local.png +0 -0
- psychopy/tests/data/text1_norm_local.png +0 -0
- psychopy/tests/data/text1_stencil_local.png +0 -0
- psychopy/tests/data/text2_height.png +0 -0
- psychopy/tests/data/text2_normAddBlend.png +0 -0
- psychopy/tests/data/text2_normHexbackground.png +0 -0
- psychopy/tests/data/text2_stencil.png +0 -0
- psychopy/tests/data/wedge1_height_local.png +0 -0
- psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
- psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
- psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
- psychopy/tests/data/wedge1_norm_local.png +0 -0
- psychopy/tests/data/wedge1_stencil_local.png +0 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
- psychopy/tests/test_data/.DS_Store +0 -0
- psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
- psychopy/tests/test_tools/test_arraytools.py +112 -0
- psychopy/tests/test_visual/test_image.py.orig +219 -0
- psychopy/tools/arraytools.py +47 -0
- psychopy/tools/versionchooser.py +1 -1
- psychopy/visual/backends/pygletbackend.py +26 -8
- psychopy/visual/basevisual.py.orig +1723 -0
- psychopy/visual/form.py.orig +1181 -0
- psychopy/visual/text.py.orig +752 -0
- psychopy/visual/textbox2/textbox2.py.orig +1315 -0
- psychopy/visual/window.py +13 -5
- psychopy/visual/windowwarp.py.orig +463 -0
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
- /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
- /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
- /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python support for `Brain Products GMBH <https://www.brainproducts.com>`_ hardware.
|
|
3
|
+
|
|
4
|
+
Here we have implemented support for the Remote Control Server application,
|
|
5
|
+
which allows you to control recordings, send annotations etc. all from Python.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import socket
|
|
9
|
+
import time
|
|
10
|
+
import threading
|
|
11
|
+
import weakref
|
|
12
|
+
from psychopy import logging
|
|
13
|
+
|
|
14
|
+
_appStates = {
|
|
15
|
+
'AP:0': 'Closed',
|
|
16
|
+
'AP:1': 'Open',
|
|
17
|
+
'AP:-1': 'Errored',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_recordingStates = {
|
|
21
|
+
'RS:0': 'Idle',
|
|
22
|
+
'RS:1': 'Monitoring',
|
|
23
|
+
'RS:2': 'Calibration',
|
|
24
|
+
'RS:3': 'Impedance check',
|
|
25
|
+
'RS:4': 'Recording', # the manual calls this Saving (recording)"
|
|
26
|
+
'RS:5': 'Saving calibration', # the manual calls this "Saving calibration"
|
|
27
|
+
'RS:6': 'Paused',
|
|
28
|
+
'RS:7': 'Paused calibration',
|
|
29
|
+
'RS:8': 'Paused impedance check',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_acquisitionStates = {
|
|
33
|
+
'AQ:0': 'Stopped',
|
|
34
|
+
'AQ:1': 'Running',
|
|
35
|
+
'AQ:2': 'Warning',
|
|
36
|
+
'AQ:3': 'Error',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RemoteControlServer:
|
|
41
|
+
"""
|
|
42
|
+
Provides a remote-control interface to BrainProducts Recorder.
|
|
43
|
+
|
|
44
|
+
Example usage::
|
|
45
|
+
|
|
46
|
+
import time
|
|
47
|
+
from psychopy import logging
|
|
48
|
+
from psychopy.hardware import brainproducts
|
|
49
|
+
|
|
50
|
+
logging.console.setLevel(logging.DEBUG)
|
|
51
|
+
rcs = brainproducts.RemoteControlServer()
|
|
52
|
+
rcs.open('testExp',
|
|
53
|
+
workspace='C:/Vision/Workfiles/Standard Workspace.rwksp',
|
|
54
|
+
participant='S0021')
|
|
55
|
+
rcs.openRecorder()
|
|
56
|
+
time.sleep(2)
|
|
57
|
+
rcs.mode = 'monitor' # or 'impedance', or 'default'
|
|
58
|
+
rcs.startRecording()
|
|
59
|
+
time.sleep(2)
|
|
60
|
+
rcs.sendAnnotation('124', 'STIM')
|
|
61
|
+
time.sleep(1)
|
|
62
|
+
rcs.pauseRecording()
|
|
63
|
+
time.sleep(1)
|
|
64
|
+
rcs.resumeRecording()
|
|
65
|
+
time.sleep(1)
|
|
66
|
+
rcs.stopRecording()
|
|
67
|
+
time.sleep(1)
|
|
68
|
+
rcs.mode = 'default' # stops monitoring mode
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, host='127.0.0.1', port=6700, timeout=1.0,
|
|
73
|
+
testMode=False):
|
|
74
|
+
"""To initialize the remote control recorder.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
host : string, optional
|
|
79
|
+
The IP address or hostname of the computer running RCS.
|
|
80
|
+
Defaults to ``127.0.0.1``.
|
|
81
|
+
port : int, optional
|
|
82
|
+
The port on which RCS is listening for a connection on the
|
|
83
|
+
EEG computer. This should usually not need to be changed.
|
|
84
|
+
Defaults to ``6700``.
|
|
85
|
+
timeout : float, optional
|
|
86
|
+
The timeout (in seconds) to wait for sending/receivign commands
|
|
87
|
+
testMode : bool, optional
|
|
88
|
+
If ``True``, the network connection to the RCS computer will
|
|
89
|
+
not actually be initialized.
|
|
90
|
+
Defaults to ``False``.
|
|
91
|
+
"""
|
|
92
|
+
self._testMode = testMode
|
|
93
|
+
|
|
94
|
+
self.applicationState = None
|
|
95
|
+
self.recordingState = None
|
|
96
|
+
self.acquisitionState = None
|
|
97
|
+
|
|
98
|
+
self._host = host
|
|
99
|
+
self._port = port
|
|
100
|
+
self._recording = False
|
|
101
|
+
self._timeout = timeout
|
|
102
|
+
|
|
103
|
+
# various properties that are initially unknown
|
|
104
|
+
self._mode = 'default'
|
|
105
|
+
self._exp_name = None
|
|
106
|
+
self._participant = None
|
|
107
|
+
self._workspace = None
|
|
108
|
+
self._amplifier = None
|
|
109
|
+
self._overwriteProtection = None
|
|
110
|
+
self._RCSversion = None
|
|
111
|
+
|
|
112
|
+
self._bufferChars = '' # unprocessed stream from RCS
|
|
113
|
+
self._bufferList = [] # list of messages
|
|
114
|
+
self._socket = socket.socket(socket.AF_INET,
|
|
115
|
+
socket.SOCK_STREAM)
|
|
116
|
+
self._socket.settimeout(self._timeout)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
self._socket.connect((self._host, self._port))
|
|
120
|
+
except socket.error:
|
|
121
|
+
if not self._testMode:
|
|
122
|
+
msg = ('Could not connect to RCS at %s:%s. Make sure the '
|
|
123
|
+
'Remote Control Server software is running and set '
|
|
124
|
+
'to "Connect"' %
|
|
125
|
+
(self._host, self._port))
|
|
126
|
+
raise RuntimeError(msg)
|
|
127
|
+
else:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
self._listener = _ListenerThread(self)
|
|
131
|
+
self._listener.start()
|
|
132
|
+
|
|
133
|
+
def sendRaw(self, message, checkOutput='OK'):
|
|
134
|
+
"""A helper function to send raw messages (strings) to the RCS.
|
|
135
|
+
|
|
136
|
+
This is normally only used for debugging purposes and is not
|
|
137
|
+
needed by most users.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
message : string
|
|
142
|
+
The string that will be sent
|
|
143
|
+
checkOutput : string (default='OK')
|
|
144
|
+
If a value is provided then this will be checked for by
|
|
145
|
+
this function. If no check is needed then set checkOutput=None
|
|
146
|
+
"""
|
|
147
|
+
# Append \r if it's not already part of the message: RCS
|
|
148
|
+
# uses this as command separators.
|
|
149
|
+
if self._testMode:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# check for reply
|
|
153
|
+
if not message.endswith('\r') or not message.endswith('\r\n'):
|
|
154
|
+
message += '\r'
|
|
155
|
+
if type(message) != bytes:
|
|
156
|
+
message = message.encode('utf-8')
|
|
157
|
+
self._socket.sendall(message)
|
|
158
|
+
|
|
159
|
+
# did reply include OK message?
|
|
160
|
+
if not checkOutput:
|
|
161
|
+
return
|
|
162
|
+
# wait for message with expected output (means OK)
|
|
163
|
+
reply = self.waitForMessage(endswith=checkOutput)
|
|
164
|
+
if not reply:
|
|
165
|
+
logging.warning(
|
|
166
|
+
"RCS Didn't receive expected response from RCS to "
|
|
167
|
+
"the message {}. Current stack of recent responses:{}."
|
|
168
|
+
.format(message, self._listener.messages))
|
|
169
|
+
logging.flush()
|
|
170
|
+
else:
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def waitForMessage(self, containing='', endswith=''):
|
|
174
|
+
"""Wait for a message, optionally one that meets certain criteria
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
containing : str
|
|
179
|
+
A string the message must contain
|
|
180
|
+
endswith : str
|
|
181
|
+
A string the message must end with (ignoring newline characters)
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
The (complete) message string if one was received or None if not
|
|
186
|
+
"""
|
|
187
|
+
# check output
|
|
188
|
+
OK = False
|
|
189
|
+
t0 = time.time()
|
|
190
|
+
while time.time() - t0 < self._timeout and not OK:
|
|
191
|
+
for reply in self._listener.messages:
|
|
192
|
+
if reply.endswith(endswith) and containing in reply:
|
|
193
|
+
logging.debug("RCS received {}".format(repr(reply)))
|
|
194
|
+
self._listener.messages.remove(reply)
|
|
195
|
+
return reply
|
|
196
|
+
|
|
197
|
+
def waitForState(self, stateName, permitted, timeout=10):
|
|
198
|
+
"""Helper function to wait for a particular state (or any attribute, for that matter)
|
|
199
|
+
to have a particular value. Beware this will wait indefinitely, so only call
|
|
200
|
+
if you are confident that the state will eventually arrive!
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
stateName : str
|
|
205
|
+
Name of the state (e.g. "applicationState")
|
|
206
|
+
permitted : list
|
|
207
|
+
List of values that are permitted before returning
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
if type(permitted) is not list:
|
|
211
|
+
raise TypeError("permitted must be a list of permitted values")
|
|
212
|
+
t0 = time.time()
|
|
213
|
+
while getattr(self, stateName) not in permitted:
|
|
214
|
+
time.sleep(0.01)
|
|
215
|
+
if time.time()-t0 > timeout:
|
|
216
|
+
logging.warning(
|
|
217
|
+
f'RCS {stateName} not achieved: expected states {permitted} but state is {getattr(self, stateName)}'
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
def open(self, expName, participant, workspace):
|
|
222
|
+
"""Opens a study/workspace on the RCS server
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
expName : str
|
|
227
|
+
Name of the experiment. Will make up the first part of the
|
|
228
|
+
EEG filename.
|
|
229
|
+
participant : str
|
|
230
|
+
Participant identifier. Will make up the second part of the
|
|
231
|
+
EEG filename.
|
|
232
|
+
workspace : str
|
|
233
|
+
The full path to the workspace file (.rwksp), with forward slashes
|
|
234
|
+
as path separators. e.g. "c:/myFolder/mySetup.rwksp"
|
|
235
|
+
"""
|
|
236
|
+
self.workspace = workspace
|
|
237
|
+
self.participant = participant
|
|
238
|
+
self.expName = expName
|
|
239
|
+
# all appears OK
|
|
240
|
+
logging.info(
|
|
241
|
+
'RCS connected: {} - {}'.format(self.expName, self.participant))
|
|
242
|
+
|
|
243
|
+
def openRecorder(self):
|
|
244
|
+
"""Opens the Recorder application from the Remote Control.
|
|
245
|
+
|
|
246
|
+
Neat, huh?!
|
|
247
|
+
"""
|
|
248
|
+
msg = 'O'
|
|
249
|
+
self.sendRaw(msg, checkOutput="O:OK")
|
|
250
|
+
# after reporting OK it should also change the status
|
|
251
|
+
self.waitForState("applicationState", ["Open"])
|
|
252
|
+
self.waitForState("recordingState", ["Idle"])
|
|
253
|
+
# check that the RCS is using the correct messaging version
|
|
254
|
+
self.sendRaw("VM", checkOutput="VM:2")
|
|
255
|
+
|
|
256
|
+
def _updateState(self, msg):
|
|
257
|
+
# Update our state variables from a state message
|
|
258
|
+
if msg[:2] == 'AP':
|
|
259
|
+
self.applicationState = _appStates[msg]
|
|
260
|
+
logging.info('RCS Recorder app is now {}'
|
|
261
|
+
.format(self.applicationState.upper()))
|
|
262
|
+
elif msg[:2] == 'RS':
|
|
263
|
+
self.recordingState = _recordingStates[msg]
|
|
264
|
+
logging.info('RCS Recorder State is now {}'
|
|
265
|
+
.format(self.recordingState.upper()))
|
|
266
|
+
elif msg[:2] == 'AQ':
|
|
267
|
+
self.acquisitionState = _acquisitionStates[msg]
|
|
268
|
+
logging.info('RCS Acq is now {}'
|
|
269
|
+
.format(self.acquisitionState.upper()))
|
|
270
|
+
else:
|
|
271
|
+
raise RuntimeError("RCS._updateState was sent unknown message"
|
|
272
|
+
"'{}'".format(msg))
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def workspace(self):
|
|
276
|
+
"""
|
|
277
|
+
Get/set the path to the workspace file. An absolute path is required.
|
|
278
|
+
|
|
279
|
+
Example Usage::
|
|
280
|
+
|
|
281
|
+
rcs.workspace = 'C:/Vision/Worksfiles/testing.rwksp'
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
return self._workspace
|
|
285
|
+
|
|
286
|
+
@workspace.setter
|
|
287
|
+
def workspace(self, path):
|
|
288
|
+
msg = '1:%s' % path
|
|
289
|
+
self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
290
|
+
|
|
291
|
+
self._workspace = path
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def expName(self):
|
|
295
|
+
"""
|
|
296
|
+
Get/set the name of the experiment or study (string)
|
|
297
|
+
|
|
298
|
+
The name will make up the first part of the EEG filename.
|
|
299
|
+
|
|
300
|
+
Example Usage::
|
|
301
|
+
|
|
302
|
+
rcs.expName = 'MyTestStudy'
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
return self._exp_name
|
|
306
|
+
|
|
307
|
+
@expName.setter
|
|
308
|
+
def expName(self, name):
|
|
309
|
+
msg = '2:%s' % name
|
|
310
|
+
self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
311
|
+
|
|
312
|
+
self._exp_name = name
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def participant(self):
|
|
316
|
+
"""
|
|
317
|
+
Get/set the participant identifier (a string or numeric).
|
|
318
|
+
|
|
319
|
+
This identifier will make up the center part of the EEG filename.
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
return self._participant
|
|
323
|
+
|
|
324
|
+
@participant.setter
|
|
325
|
+
def participant(self, participant):
|
|
326
|
+
msg = '3:{}'.format(participant)
|
|
327
|
+
self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
328
|
+
# keep track of the change
|
|
329
|
+
self._participant = participant
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def mode(self):
|
|
333
|
+
"""
|
|
334
|
+
Get/set the current mode.
|
|
335
|
+
|
|
336
|
+
Mode is a string that can be one of:
|
|
337
|
+
|
|
338
|
+
- 'default' or 'def' or None will exit special modes
|
|
339
|
+
- 'impedance' or 'imp' for impedance checking
|
|
340
|
+
- 'monitoring' or 'mon'
|
|
341
|
+
- 'test' or 'tes' to go into test view
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
return self._mode
|
|
345
|
+
|
|
346
|
+
@mode.setter
|
|
347
|
+
def mode(self, mode):
|
|
348
|
+
if mode in ['impedance', 'imp']:
|
|
349
|
+
if self.recordingState == "Recording":
|
|
350
|
+
finalRecordingState = "Paused impedance check"
|
|
351
|
+
else:
|
|
352
|
+
finalRecordingState = "Impedance check"
|
|
353
|
+
self._mode = 'impedance'
|
|
354
|
+
msg = 'I'
|
|
355
|
+
elif mode in ['monitor', 'mon']:
|
|
356
|
+
self._mode = 'monitor'
|
|
357
|
+
msg = 'M'
|
|
358
|
+
elif mode in ['test', 'tes']:
|
|
359
|
+
self._mode = 'test'
|
|
360
|
+
msg = 'T'
|
|
361
|
+
elif mode in ['default', 'def', None]:
|
|
362
|
+
self._mode = 'default'
|
|
363
|
+
msg = 'SV'
|
|
364
|
+
else:
|
|
365
|
+
msg = ('`mode` must be one of: impedance, imp, monitor, mon, test '
|
|
366
|
+
'def, or default.')
|
|
367
|
+
raise ValueError(msg)
|
|
368
|
+
|
|
369
|
+
replyOK = self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
370
|
+
if not replyOK:
|
|
371
|
+
raise IOError(f"Failed to set RCS into mode {mode}. RCS did not reply 'OK'")
|
|
372
|
+
|
|
373
|
+
# now wait for appropriate state changes to match our target mode
|
|
374
|
+
if mode in ['impedance', 'imp']:
|
|
375
|
+
self.waitForState("recordingState", [finalRecordingState])
|
|
376
|
+
self.waitForState("acquisitionState", ["Running"])
|
|
377
|
+
elif mode in ['monitor', 'mon']:
|
|
378
|
+
self.waitForState("recordingState", ["Monitoring"])
|
|
379
|
+
self.waitForState("acquisitionState", ["Running"])
|
|
380
|
+
elif mode in ['test', 'tes']:
|
|
381
|
+
<<<<<<< HEAD
|
|
382
|
+
self.waitForState("recordingState", ["Calibration"])
|
|
383
|
+
=======
|
|
384
|
+
self.waitForState("recordingState", ["Monitoring"])
|
|
385
|
+
>>>>>>> release
|
|
386
|
+
self.waitForState("acquisitionState", ["Running"])
|
|
387
|
+
elif mode in ['default', 'def', None]:
|
|
388
|
+
self.waitForState("recordingState", ["Idle"])
|
|
389
|
+
self.waitForState("acquisitionState", ["Stopped"])
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def timeout(self):
|
|
393
|
+
"""What is a reasonable timeout in seconds (initially set to 0.5)
|
|
394
|
+
|
|
395
|
+
For some systems (e.g. when the RCS is the same machine) you might want
|
|
396
|
+
to set this to a lower value. For an unpredictable or slow network
|
|
397
|
+
connection you might want to set this to a higher value.
|
|
398
|
+
"""
|
|
399
|
+
return self._timeout
|
|
400
|
+
|
|
401
|
+
@timeout.setter
|
|
402
|
+
def timeout(self, timeout):
|
|
403
|
+
self._socket.settimeout(timeout)
|
|
404
|
+
self._timeout = timeout
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def amplifier(self):
|
|
408
|
+
"""Get/set the amplifier to use. Could be one of
|
|
409
|
+
" ['actiCHamp', 'BrainAmp Family',"
|
|
410
|
+
" 'LiveAmp', 'QuickAmp USB', 'Simulated Amplifier',"
|
|
411
|
+
" 'V-Amp / FirstAmp']
|
|
412
|
+
|
|
413
|
+
For Liveamp you should also provide the serial number,
|
|
414
|
+
comma separated from the amplifier type.
|
|
415
|
+
|
|
416
|
+
Examples:
|
|
417
|
+
rcs = RemoteControlServer()
|
|
418
|
+
rcs.amplifier = 'LiveAmp', 'LA-05490-0200'
|
|
419
|
+
# OR
|
|
420
|
+
rcs.amplifier = 'actiCHamp'
|
|
421
|
+
"""
|
|
422
|
+
return self._amplifier
|
|
423
|
+
|
|
424
|
+
@amplifier.setter
|
|
425
|
+
def amplifier(self, amplifier):
|
|
426
|
+
# did we get a tuple/list of ampType, ampSN or just name?
|
|
427
|
+
serialNumber = None
|
|
428
|
+
if len(amplifier) == 2: # e.g. ('LiveAmp', '34834727')
|
|
429
|
+
amplifier, serialNumber = amplifier
|
|
430
|
+
elif len(amplifier) == 1: # e.g. ('actiCHamp')
|
|
431
|
+
amplifier = amplifier[0] # extract string from tuple/list
|
|
432
|
+
else:
|
|
433
|
+
assert type(amplifier) == str # hopefully then we got the name raw
|
|
434
|
+
# check for LiveAmp that we also have a SN
|
|
435
|
+
if amplifier == 'LiveAmp' and not serialNumber:
|
|
436
|
+
logging.warning("LiveAmp may need a serial number. Use\n"
|
|
437
|
+
" rcs.amplifier = 'LiveAmp', 'LA-serialNumberHere'")
|
|
438
|
+
logging.flush()
|
|
439
|
+
if amplifier in ['actiCHamp', 'BrainAmp Family',
|
|
440
|
+
'LiveAmp', 'QuickAmp USB', 'Simulated Amplifier',
|
|
441
|
+
'V-Amp / FirstAmp']:
|
|
442
|
+
msg = "SA:{}".format(amplifier)
|
|
443
|
+
self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
444
|
+
else:
|
|
445
|
+
errMsg = (f"Unknown amplifier '{amplifier}'. The `amplifier` value "
|
|
446
|
+
"should be a LiveAmp serial number or one of "
|
|
447
|
+
"['actiCHamp', 'BrainAmp Family',"
|
|
448
|
+
" 'LiveAmp', 'QuickAmp USB', 'Simulated Amplifier',"
|
|
449
|
+
" 'V-Amp / FirstAmp']")
|
|
450
|
+
raise ValueError(errMsg)
|
|
451
|
+
if serialNumber:
|
|
452
|
+
# LiveAmp allows you to send the serial number
|
|
453
|
+
msg = "SN:{}".format(serialNumber)
|
|
454
|
+
self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
455
|
+
self._amplifier = amplifier
|
|
456
|
+
self._amplifierSN = serialNumber
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def overwriteProtection(self):
|
|
460
|
+
"""An attribute to get/set whether the overwrite protection is turned on.
|
|
461
|
+
|
|
462
|
+
When checking the attribute the state of `rcs.overwriteProtection` a call will be
|
|
463
|
+
made to the RCS and the report is based on the response. There is also a
|
|
464
|
+
variable `rcs._overwriteProtection` that is simply the stored state from the
|
|
465
|
+
most recent call and does not make any further communication with the RCS itself.
|
|
466
|
+
|
|
467
|
+
Usage example::
|
|
468
|
+
|
|
469
|
+
rcs.overwriteProtection = True # set it to be on
|
|
470
|
+
print(rcs.overwriteProtection) # print current state
|
|
471
|
+
"""
|
|
472
|
+
reply = self.sendRaw("OW", checkOutput=None) # we'll check this one manually
|
|
473
|
+
# reply is OW:0:OK or OW:1:OK
|
|
474
|
+
if reply == 'OW:0:OK':
|
|
475
|
+
state = False
|
|
476
|
+
elif reply == 'OW:1:OK':
|
|
477
|
+
state = True
|
|
478
|
+
else:
|
|
479
|
+
raise IOError("Request for overwrite state received unknown"
|
|
480
|
+
"response '{}'".format(reply))
|
|
481
|
+
self._overwriteProtection = state
|
|
482
|
+
return self._overwriteProtection
|
|
483
|
+
|
|
484
|
+
@overwriteProtection.setter
|
|
485
|
+
def overwriteProtection(self, value):
|
|
486
|
+
if value not in [True, False]: # or 1, 0 not necess bool type
|
|
487
|
+
raise ValueError("RCS.overwriteProtection should be set to "
|
|
488
|
+
"True or False, not '{}'".format(value))
|
|
489
|
+
msg = "OW:{}".format(int(value))
|
|
490
|
+
self.sendRaw(msg, checkOutput=msg + ':OK')
|
|
491
|
+
self._overwriteProtection = bool(value)
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def version(self):
|
|
495
|
+
"""Reports the version of the RCS application
|
|
496
|
+
|
|
497
|
+
Example usage::
|
|
498
|
+
|
|
499
|
+
print(rcs.version)
|
|
500
|
+
|
|
501
|
+
"""
|
|
502
|
+
if not self._RCSversion:
|
|
503
|
+
# otherwise request info from RCS
|
|
504
|
+
msg = 'VS'
|
|
505
|
+
self.sendRaw(msg, checkOutput='')
|
|
506
|
+
reply = self.waitForMessage(containing='VS:')
|
|
507
|
+
if reply:
|
|
508
|
+
self._RCSversion = reply.strip().replace("VS:")
|
|
509
|
+
else:
|
|
510
|
+
logging.warning("Failed to retrieve the version of the RCS software")
|
|
511
|
+
logging.flush()
|
|
512
|
+
return self._RCSversion
|
|
513
|
+
|
|
514
|
+
def dcReset(self):
|
|
515
|
+
"""Use this to reset any DC offset that might have accumulated
|
|
516
|
+
if you aren't using a high-pass filter"""
|
|
517
|
+
msg = 'D'
|
|
518
|
+
self.sendRaw(msg)
|
|
519
|
+
|
|
520
|
+
def startRecording(self):
|
|
521
|
+
"""
|
|
522
|
+
Start recording EEG.
|
|
523
|
+
|
|
524
|
+
"""
|
|
525
|
+
recordingType = self.recordingState
|
|
526
|
+
if recordingType not in ['Monitoring', 'Calibration', 'Impedance check']:
|
|
527
|
+
msg = ('To start recording, the RCS must be in one of "Monitoring", '
|
|
528
|
+
f'"Calibration" or "Impedance check" states, not {recordingType}')
|
|
529
|
+
raise RuntimeError(msg)
|
|
530
|
+
if self._recording:
|
|
531
|
+
msg = 'Recording is already in progress!'
|
|
532
|
+
raise RuntimeError(msg)
|
|
533
|
+
|
|
534
|
+
msg = 'S'
|
|
535
|
+
self.sendRaw(msg)
|
|
536
|
+
|
|
537
|
+
self.waitForState("recordingState", ["Recording", "Saving calibration"])
|
|
538
|
+
self._recording = True
|
|
539
|
+
|
|
540
|
+
def stopRecording(self):
|
|
541
|
+
"""
|
|
542
|
+
Stop recording EEG.
|
|
543
|
+
|
|
544
|
+
"""
|
|
545
|
+
if not self._recording:
|
|
546
|
+
msg = 'Recording has not yet been started!'
|
|
547
|
+
raise RuntimeError(msg)
|
|
548
|
+
|
|
549
|
+
msg = 'Q'
|
|
550
|
+
self.sendRaw(msg)
|
|
551
|
+
self.waitForState("recordingState", ["Recording", "Calibration"])
|
|
552
|
+
self._recording = False
|
|
553
|
+
|
|
554
|
+
def pauseRecording(self):
|
|
555
|
+
"""
|
|
556
|
+
Pause recording EEG without ending the session.
|
|
557
|
+
|
|
558
|
+
"""
|
|
559
|
+
msg = 'P'
|
|
560
|
+
self.sendRaw(msg)
|
|
561
|
+
self.waitForState("recordingState", ["Paused", "Paused calibration"])
|
|
562
|
+
|
|
563
|
+
def resumeRecording(self):
|
|
564
|
+
"""
|
|
565
|
+
Resume a paused recording
|
|
566
|
+
|
|
567
|
+
"""
|
|
568
|
+
msg = 'C'
|
|
569
|
+
self.sendRaw(msg)
|
|
570
|
+
self.waitForState("recordingState", ["Recording", "Saving calibration"])
|
|
571
|
+
|
|
572
|
+
def sendAnnotation(self, annotation, annType):
|
|
573
|
+
"""Sends a message to be logged on the Recorder.
|
|
574
|
+
|
|
575
|
+
The timing of annotations may be imprecise and this
|
|
576
|
+
should not be trusted as a method of sending sync triggers.
|
|
577
|
+
|
|
578
|
+
Annotations can contain any ASCII characters except for ";"
|
|
579
|
+
|
|
580
|
+
Parameters
|
|
581
|
+
-----------------
|
|
582
|
+
|
|
583
|
+
annotation : string
|
|
584
|
+
The description text to be sent in the annotation.
|
|
585
|
+
|
|
586
|
+
annType : string
|
|
587
|
+
The category of the annotation which are user-defined
|
|
588
|
+
strings (e.g. stimulus, response)
|
|
589
|
+
|
|
590
|
+
Example usage::
|
|
591
|
+
|
|
592
|
+
rcs.sendAnnotation("face003", "stimulus")
|
|
593
|
+
|
|
594
|
+
"""
|
|
595
|
+
msg = "AN:{};{}".format(annotation, annType)
|
|
596
|
+
self.sendRaw(msg)
|
|
597
|
+
|
|
598
|
+
def close(self):
|
|
599
|
+
"""Closes the recording and deletes all associated workspace
|
|
600
|
+
variables (e.g. when a participant has been completed)
|
|
601
|
+
"""
|
|
602
|
+
msg = 'X'
|
|
603
|
+
self.sendRaw(msg)
|
|
604
|
+
self.waitForState("recordingState", ["Idle"])
|
|
605
|
+
self.waitForState("acquisitionState", ["Stopped"])
|
|
606
|
+
self.waitForState("applicationState", ["Closed"])
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class _ListenerThread(threading.Thread):
|
|
610
|
+
def __init__(self, parent):
|
|
611
|
+
self._socket = parent._socket # type: socket.socket
|
|
612
|
+
self.messages = []
|
|
613
|
+
self._buffer = ''
|
|
614
|
+
threading.Thread.__init__(self, daemon=True)
|
|
615
|
+
self._parentRef = weakref.ref(parent)
|
|
616
|
+
self._is_running = None
|
|
617
|
+
|
|
618
|
+
def run(self):
|
|
619
|
+
"""Gets run repeatedly until terminates
|
|
620
|
+
"""
|
|
621
|
+
if self._is_running is None:
|
|
622
|
+
self._is_running = True
|
|
623
|
+
while self._is_running:
|
|
624
|
+
try:
|
|
625
|
+
if self._socket._closed:
|
|
626
|
+
break
|
|
627
|
+
recvd = self._socket.recv(512).decode('utf-8')
|
|
628
|
+
self._buffer += recvd
|
|
629
|
+
self.processBuffer()
|
|
630
|
+
except socket.timeout:
|
|
631
|
+
time.sleep(0.1)
|
|
632
|
+
except OSError:
|
|
633
|
+
if self._socket._closed:
|
|
634
|
+
self._is_running = False
|
|
635
|
+
|
|
636
|
+
def processBuffer(self):
|
|
637
|
+
|
|
638
|
+
# check for whole messages:
|
|
639
|
+
nMessages = self._buffer.count('\r')
|
|
640
|
+
msgList = self._buffer.split('\r')
|
|
641
|
+
for msgN in range(nMessages):
|
|
642
|
+
thisMsg = msgList[msgN]
|
|
643
|
+
# remove message from buffer so we don't reuse
|
|
644
|
+
self._buffer = self._buffer.replace(thisMsg + '\r', '')
|
|
645
|
+
# check if the message is a change of state
|
|
646
|
+
if thisMsg[:2] in ['AP', 'RS', 'AQ']:
|
|
647
|
+
self._parentRef()._updateState(thisMsg)
|
|
648
|
+
else:
|
|
649
|
+
self.messages.append(thisMsg)
|
|
650
|
+
|
|
651
|
+
def clear(self):
|
|
652
|
+
self.messages = []
|
|
653
|
+
self._buffer = ''
|
|
654
|
+
while True:
|
|
655
|
+
try:
|
|
656
|
+
self._socket.recv(1)
|
|
657
|
+
except socket.timeout: # no chars left to clear
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
if __name__ == "__main__":
|
|
662
|
+
logging.console.setLevel(logging.DEBUG)
|
|
663
|
+
rcs = RemoteControlServer()
|
|
664
|
+
rcs.open('testExp',
|
|
665
|
+
workspace='C:/Vision/Workfiles/Standard Workspace.rwksp',
|
|
666
|
+
participant='S0021')
|
|
667
|
+
rcs.openRecorder()
|
|
668
|
+
time.sleep(2)
|
|
669
|
+
rcs.mode = 'monitor' # or 'impedance', or 'default'
|
|
670
|
+
rcs.startRecording()
|
|
671
|
+
time.sleep(2)
|
|
672
|
+
rcs.sendAnnotation('124', 'STIM')
|
|
673
|
+
time.sleep(1)
|
|
674
|
+
rcs.pauseRecording()
|
|
675
|
+
time.sleep(1)
|
|
676
|
+
rcs.resumeRecording()
|
|
677
|
+
time.sleep(1)
|
|
678
|
+
rcs.stopRecording()
|
|
679
|
+
time.sleep(1)
|
|
680
|
+
rcs.mode = 'default' # stops monitoring mode
|