py-neuromodulation 0.0.4__py3-none-any.whl → 0.0.5__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.
- py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -34
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -106
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -119
- py_neuromodulation/FieldTrip.py +589 -589
- py_neuromodulation/__init__.py +74 -13
- py_neuromodulation/_write_example_dataset_helper.py +83 -65
- py_neuromodulation/data/README +6 -6
- py_neuromodulation/data/dataset_description.json +8 -8
- py_neuromodulation/data/participants.json +32 -32
- py_neuromodulation/data/participants.tsv +2 -2
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
- py_neuromodulation/grid_cortex.tsv +40 -40
- py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
- py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
- py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
- py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/nm_IO.py +413 -417
- py_neuromodulation/nm_RMAP.py +496 -531
- py_neuromodulation/nm_analysis.py +993 -1074
- py_neuromodulation/nm_artifacts.py +30 -25
- py_neuromodulation/nm_bispectra.py +154 -168
- py_neuromodulation/nm_bursts.py +292 -198
- py_neuromodulation/nm_coherence.py +251 -205
- py_neuromodulation/nm_database.py +149 -0
- py_neuromodulation/nm_decode.py +918 -992
- py_neuromodulation/nm_define_nmchannels.py +300 -302
- py_neuromodulation/nm_features.py +144 -116
- py_neuromodulation/nm_filter.py +219 -219
- py_neuromodulation/nm_filter_preprocessing.py +79 -91
- py_neuromodulation/nm_fooof.py +139 -159
- py_neuromodulation/nm_generator.py +45 -37
- py_neuromodulation/nm_hjorth_raw.py +52 -73
- py_neuromodulation/nm_kalmanfilter.py +71 -58
- py_neuromodulation/nm_linelength.py +21 -33
- py_neuromodulation/nm_logger.py +66 -0
- py_neuromodulation/nm_mne_connectivity.py +149 -112
- py_neuromodulation/nm_mnelsl_generator.py +90 -0
- py_neuromodulation/nm_mnelsl_stream.py +116 -0
- py_neuromodulation/nm_nolds.py +96 -93
- py_neuromodulation/nm_normalization.py +173 -214
- py_neuromodulation/nm_oscillatory.py +423 -448
- py_neuromodulation/nm_plots.py +585 -612
- py_neuromodulation/nm_preprocessing.py +83 -0
- py_neuromodulation/nm_projection.py +370 -394
- py_neuromodulation/nm_rereference.py +97 -95
- py_neuromodulation/nm_resample.py +59 -50
- py_neuromodulation/nm_run_analysis.py +325 -435
- py_neuromodulation/nm_settings.py +289 -68
- py_neuromodulation/nm_settings.yaml +244 -0
- py_neuromodulation/nm_sharpwaves.py +423 -401
- py_neuromodulation/nm_stats.py +464 -480
- py_neuromodulation/nm_stream.py +398 -0
- py_neuromodulation/nm_stream_abc.py +166 -218
- py_neuromodulation/nm_types.py +193 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.5.dist-info}/METADATA +29 -26
- py_neuromodulation-0.0.5.dist-info/RECORD +83 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.5.dist-info}/WHEEL +1 -1
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.5.dist-info}/licenses/LICENSE +21 -21
- py_neuromodulation/nm_EpochStream.py +0 -92
- py_neuromodulation/nm_across_patient_decoding.py +0 -927
- py_neuromodulation/nm_cohortwrapper.py +0 -435
- py_neuromodulation/nm_eval_timing.py +0 -239
- py_neuromodulation/nm_features_abc.py +0 -39
- py_neuromodulation/nm_settings.json +0 -338
- py_neuromodulation/nm_stream_offline.py +0 -359
- py_neuromodulation/utils/_logging.py +0 -24
- py_neuromodulation-0.0.4.dist-info/RECORD +0 -72
py_neuromodulation/FieldTrip.py
CHANGED
|
@@ -1,589 +1,589 @@
|
|
|
1
|
-
"""
|
|
2
|
-
FieldTrip buffer (V1) client in pure Python
|
|
3
|
-
(C) 2010 S. Klanke
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
# Obtained from https://github.com/fieldtrip/fieldtrip/blob/master/realtime/src/buffer/python/FieldTrip.py
|
|
7
|
-
|
|
8
|
-
# We need socket, struct, and numpy
|
|
9
|
-
import socket
|
|
10
|
-
import struct
|
|
11
|
-
import numpy
|
|
12
|
-
import unicodedata
|
|
13
|
-
|
|
14
|
-
VERSION = 1
|
|
15
|
-
|
|
16
|
-
PUT_HDR = 0x0101
|
|
17
|
-
PUT_DAT = 0x0102
|
|
18
|
-
PUT_EVT = 0x0103
|
|
19
|
-
PUT_OK = 0x0104
|
|
20
|
-
PUT_ERR = 0x0105
|
|
21
|
-
GET_HDR = 0x0201
|
|
22
|
-
GET_DAT = 0x0202
|
|
23
|
-
GET_EVT = 0x0203
|
|
24
|
-
GET_OK = 0x0204
|
|
25
|
-
GET_ERR = 0x0205
|
|
26
|
-
FLUSH_HDR = 0x0301
|
|
27
|
-
FLUSH_DAT = 0x0302
|
|
28
|
-
FLUSH_EVT = 0x0303
|
|
29
|
-
FLUSH_OK = 0x0304
|
|
30
|
-
FLUSH_ERR = 0x0305
|
|
31
|
-
WAIT_DAT = 0x0402
|
|
32
|
-
WAIT_OK = 0x0404
|
|
33
|
-
WAIT_ERR = 0x0405
|
|
34
|
-
PUT_HDR_NORESPONSE = 0x0501
|
|
35
|
-
PUT_DAT_NORESPONSE = 0x0502
|
|
36
|
-
PUT_EVT_NORESPONSE = 0x0503
|
|
37
|
-
|
|
38
|
-
DATATYPE_CHAR = 0
|
|
39
|
-
DATATYPE_UINT8 = 1
|
|
40
|
-
DATATYPE_UINT16 = 2
|
|
41
|
-
DATATYPE_UINT32 = 3
|
|
42
|
-
DATATYPE_UINT64 = 4
|
|
43
|
-
DATATYPE_INT8 = 5
|
|
44
|
-
DATATYPE_INT16 = 6
|
|
45
|
-
DATATYPE_INT32 = 7
|
|
46
|
-
DATATYPE_INT64 = 8
|
|
47
|
-
DATATYPE_FLOAT32 = 9
|
|
48
|
-
DATATYPE_FLOAT64 = 10
|
|
49
|
-
DATATYPE_UNKNOWN = 0xFFFFFFFF
|
|
50
|
-
|
|
51
|
-
CHUNK_UNSPECIFIED = 0
|
|
52
|
-
CHUNK_CHANNEL_NAMES = 1
|
|
53
|
-
CHUNK_CHANNEL_FLAGS = 2
|
|
54
|
-
CHUNK_RESOLUTIONS = 3
|
|
55
|
-
CHUNK_ASCII_KEYVAL = 4
|
|
56
|
-
CHUNK_NIFTI1 = 5
|
|
57
|
-
CHUNK_SIEMENS_AP = 6
|
|
58
|
-
CHUNK_CTF_RES4 = 7
|
|
59
|
-
CHUNK_NEUROMAG_FIF = 8
|
|
60
|
-
CHUNK_NEUROMAG_ISOTRAK = 9
|
|
61
|
-
CHUNK_NEUROMAG_HPIRESULT = 10
|
|
62
|
-
|
|
63
|
-
# List for converting FieldTrip datatypes to Numpy datatypes
|
|
64
|
-
numpyType = ['int8', 'uint8', 'uint16', 'uint32', 'uint64',
|
|
65
|
-
'int8', 'int16', 'int32', 'int64', 'float32', 'float64']
|
|
66
|
-
# Corresponding word sizes
|
|
67
|
-
wordSize = [1, 1, 2, 4, 8, 1, 2, 4, 8, 4, 8]
|
|
68
|
-
# FieldTrip data type as indexed by numpy dtype.num
|
|
69
|
-
# this goes 0 => nothing, 1..4 => int8, uint8, int16, uint16, 7..10 =>
|
|
70
|
-
# int32, uint32, int64, uint64 11..12 => float32, float64
|
|
71
|
-
dataType = [-1, 5, 1, 6, 2, -1, -1, 7, 3, 8, 4, 9, 10]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def serialize(A):
|
|
75
|
-
"""
|
|
76
|
-
Returns FieldTrip data type and string representation of the given
|
|
77
|
-
object, if possible.
|
|
78
|
-
"""
|
|
79
|
-
if isinstance(A, str):
|
|
80
|
-
return (0, A)
|
|
81
|
-
|
|
82
|
-
if isinstance(A, numpy.ndarray):
|
|
83
|
-
dt = A.dtype
|
|
84
|
-
if not(dt.isnative) or dt.num < 1 or dt.num >= len(dataType):
|
|
85
|
-
return (DATATYPE_UNKNOWN, None)
|
|
86
|
-
|
|
87
|
-
ft = dataType[dt.num]
|
|
88
|
-
if ft == -1:
|
|
89
|
-
return (DATATYPE_UNKNOWN, None)
|
|
90
|
-
|
|
91
|
-
if A.flags['C_CONTIGUOUS']:
|
|
92
|
-
# great, just use the array's buffer interface
|
|
93
|
-
return (ft, A.tostring())
|
|
94
|
-
|
|
95
|
-
# otherwise, we need a copy to C order
|
|
96
|
-
AC = A.copy('C')
|
|
97
|
-
return (ft, AC.tostring())
|
|
98
|
-
|
|
99
|
-
if isinstance(A, int):
|
|
100
|
-
return (DATATYPE_INT32, struct.pack('i', A))
|
|
101
|
-
|
|
102
|
-
if isinstance(A, float):
|
|
103
|
-
return (DATATYPE_FLOAT64, struct.pack('d', A))
|
|
104
|
-
|
|
105
|
-
return (DATATYPE_UNKNOWN, None)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class Chunk:
|
|
109
|
-
|
|
110
|
-
def __init__(self):
|
|
111
|
-
self.type = 0
|
|
112
|
-
self.size = 0
|
|
113
|
-
self.buf = ''
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class Header:
|
|
117
|
-
|
|
118
|
-
"""Class for storing header information in the FieldTrip buffer format"""
|
|
119
|
-
|
|
120
|
-
def __init__(self):
|
|
121
|
-
self.nChannels = 0
|
|
122
|
-
self.nSamples = 0
|
|
123
|
-
self.nEvents = 0
|
|
124
|
-
self.fSample = 0.0
|
|
125
|
-
self.dataType = 0
|
|
126
|
-
self.chunks = {}
|
|
127
|
-
self.labels = []
|
|
128
|
-
|
|
129
|
-
def __str__(self):
|
|
130
|
-
return ('Channels.: %i\nSamples..: %i\nEvents...: %i\nSampFreq.: '
|
|
131
|
-
'%f\nDataType.: %s\n'
|
|
132
|
-
% (self.nChannels, self.nSamples, self.nEvents,
|
|
133
|
-
self.fSample, numpyType[self.dataType]))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
class Event:
|
|
137
|
-
"""Class for storing events in the FieldTrip buffer format"""
|
|
138
|
-
|
|
139
|
-
def __init__(self, S=None):
|
|
140
|
-
if S is None:
|
|
141
|
-
self.type = ''
|
|
142
|
-
self.value = ''
|
|
143
|
-
self.sample = 0
|
|
144
|
-
self.offset = 0
|
|
145
|
-
self.duration = 0
|
|
146
|
-
else:
|
|
147
|
-
self.deserialize(S)
|
|
148
|
-
|
|
149
|
-
def __str__(self):
|
|
150
|
-
return ('Type.....: %s\nValue....: %s\nSample...: %i\nOffset...: '
|
|
151
|
-
'%i\nDuration.: %i\n' % (str(self.type), str(self.value),
|
|
152
|
-
self.sample, self.offset,
|
|
153
|
-
self.duration))
|
|
154
|
-
|
|
155
|
-
def deserialize(self, buf):
|
|
156
|
-
bufsize = len(buf)
|
|
157
|
-
if bufsize < 32:
|
|
158
|
-
return 0
|
|
159
|
-
|
|
160
|
-
(type_type, type_numel, value_type, value_numel, sample,
|
|
161
|
-
offset, duration, bsiz) = struct.unpack('IIIIIiiI', buf[0:32])
|
|
162
|
-
|
|
163
|
-
self.sample = sample
|
|
164
|
-
self.offset = offset
|
|
165
|
-
self.duration = duration
|
|
166
|
-
|
|
167
|
-
st = type_numel * wordSize[type_type]
|
|
168
|
-
sv = value_numel * wordSize[value_type]
|
|
169
|
-
|
|
170
|
-
if bsiz + 32 > bufsize or st + sv > bsiz:
|
|
171
|
-
raise IOError(
|
|
172
|
-
'Invalid event definition -- does not fit in given buffer')
|
|
173
|
-
|
|
174
|
-
raw_type = buf[32:32 + st]
|
|
175
|
-
raw_value = buf[32 + st:32 + st + sv]
|
|
176
|
-
|
|
177
|
-
if type_type == 0:
|
|
178
|
-
self.type = raw_type
|
|
179
|
-
else:
|
|
180
|
-
self.type = numpy.ndarray(
|
|
181
|
-
(type_numel), dtype=numpyType[type_type], buffer=raw_type)
|
|
182
|
-
|
|
183
|
-
if value_type == 0:
|
|
184
|
-
self.value = raw_value
|
|
185
|
-
else:
|
|
186
|
-
self.value = numpy.ndarray(
|
|
187
|
-
(value_numel), dtype=numpyType[value_type], buffer=raw_value)
|
|
188
|
-
|
|
189
|
-
return bsiz + 32
|
|
190
|
-
|
|
191
|
-
def serialize(self):
|
|
192
|
-
"""
|
|
193
|
-
Returns the contents of this event as a string, ready to
|
|
194
|
-
send over the network, or None in case of conversion problems.
|
|
195
|
-
"""
|
|
196
|
-
type_type, type_buf = serialize(self.type)
|
|
197
|
-
if type_type == DATATYPE_UNKNOWN:
|
|
198
|
-
return None
|
|
199
|
-
type_size = len(type_buf)
|
|
200
|
-
type_numel = type_size / wordSize[type_type]
|
|
201
|
-
|
|
202
|
-
value_type, value_buf = serialize(self.value)
|
|
203
|
-
if value_type == DATATYPE_UNKNOWN:
|
|
204
|
-
return None
|
|
205
|
-
value_size = len(value_buf)
|
|
206
|
-
value_numel = value_size / wordSize[value_type]
|
|
207
|
-
|
|
208
|
-
bufsize = type_size + value_size
|
|
209
|
-
|
|
210
|
-
S = struct.pack('IIIIIiiI', type_type, type_numel, value_type,
|
|
211
|
-
value_numel, int(self.sample), int(self.offset),
|
|
212
|
-
int(self.duration), bufsize)
|
|
213
|
-
return S + type_buf + value_buf
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
class Client:
|
|
217
|
-
|
|
218
|
-
"""Class for managing a client connection to a FieldTrip buffer."""
|
|
219
|
-
|
|
220
|
-
def __init__(self):
|
|
221
|
-
self.isConnected = False
|
|
222
|
-
self.sock = []
|
|
223
|
-
|
|
224
|
-
def connect(self, hostname, port=1972):
|
|
225
|
-
"""
|
|
226
|
-
connect(hostname [, port]) -- make a connection, default port is
|
|
227
|
-
1972.
|
|
228
|
-
"""
|
|
229
|
-
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
230
|
-
self.sock.connect((hostname, port))
|
|
231
|
-
self.sock.setblocking(True)
|
|
232
|
-
self.isConnected = True
|
|
233
|
-
|
|
234
|
-
def disconnect(self):
|
|
235
|
-
"""disconnect() -- close a connection."""
|
|
236
|
-
if self.isConnected:
|
|
237
|
-
self.sock.close()
|
|
238
|
-
self.sock = []
|
|
239
|
-
self.isConnected = False
|
|
240
|
-
|
|
241
|
-
def sendRaw(self, request):
|
|
242
|
-
"""Send all bytes of the string 'request' out to socket."""
|
|
243
|
-
if not(self.isConnected):
|
|
244
|
-
raise IOError('Not connected to FieldTrip buffer')
|
|
245
|
-
|
|
246
|
-
N = len(request)
|
|
247
|
-
nw = self.sock.send(request)
|
|
248
|
-
while nw < N:
|
|
249
|
-
nw += self.sock.send(request[nw:])
|
|
250
|
-
|
|
251
|
-
def sendRequest(self, command, payload=None):
|
|
252
|
-
if payload is None:
|
|
253
|
-
request = struct.pack('HHI', VERSION, command, 0)
|
|
254
|
-
else:
|
|
255
|
-
request = struct.pack(
|
|
256
|
-
'HHI', VERSION, command, len(payload)) + payload
|
|
257
|
-
self.sendRaw(request)
|
|
258
|
-
|
|
259
|
-
def receiveResponse(self, minBytes=0):
|
|
260
|
-
"""
|
|
261
|
-
Receive response from server on socket 's' and return it as
|
|
262
|
-
(status,bufsize,payload).
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
resp_hdr = self.sock.recv(8)
|
|
266
|
-
while len(resp_hdr) < 8:
|
|
267
|
-
resp_hdr += self.sock.recv(8 - len(resp_hdr))
|
|
268
|
-
|
|
269
|
-
(version, command, bufsize) = struct.unpack('HHI', resp_hdr)
|
|
270
|
-
|
|
271
|
-
if version != VERSION:
|
|
272
|
-
self.disconnect()
|
|
273
|
-
raise IOError('Bad response from buffer server - disconnecting')
|
|
274
|
-
|
|
275
|
-
if bufsize > 0:
|
|
276
|
-
payload = self.sock.recv(bufsize)
|
|
277
|
-
while len(payload) < bufsize:
|
|
278
|
-
payload += self.sock.recv(bufsize - len(payload))
|
|
279
|
-
else:
|
|
280
|
-
payload = None
|
|
281
|
-
return (command, bufsize, payload)
|
|
282
|
-
|
|
283
|
-
def getHeader(self):
|
|
284
|
-
"""
|
|
285
|
-
getHeader() -- grabs header information from the buffer an returns
|
|
286
|
-
it as a Header object.
|
|
287
|
-
"""
|
|
288
|
-
|
|
289
|
-
self.sendRequest(GET_HDR)
|
|
290
|
-
(status, bufsize, payload) = self.receiveResponse()
|
|
291
|
-
|
|
292
|
-
if status == GET_ERR:
|
|
293
|
-
return None
|
|
294
|
-
|
|
295
|
-
if status != GET_OK:
|
|
296
|
-
self.disconnect()
|
|
297
|
-
raise IOError('Bad response from buffer server - disconnecting')
|
|
298
|
-
|
|
299
|
-
if bufsize < 24:
|
|
300
|
-
self.disconnect()
|
|
301
|
-
raise IOError('Invalid HEADER packet received (too few bytes) - '
|
|
302
|
-
'disconnecting')
|
|
303
|
-
|
|
304
|
-
(nchans, nsamp, nevt, fsamp, dtype,
|
|
305
|
-
bfsiz) = struct.unpack('IIIfII', payload[0:24])
|
|
306
|
-
|
|
307
|
-
H = Header()
|
|
308
|
-
H.nChannels = nchans
|
|
309
|
-
H.nSamples = nsamp
|
|
310
|
-
H.nEvents = nevt
|
|
311
|
-
H.fSample = fsamp
|
|
312
|
-
H.dataType = dtype
|
|
313
|
-
|
|
314
|
-
if bfsiz > 0:
|
|
315
|
-
offset = 24
|
|
316
|
-
while offset + 8 < bufsize:
|
|
317
|
-
(chunk_type, chunk_len) = struct.unpack(
|
|
318
|
-
'II', payload[offset:offset + 8])
|
|
319
|
-
offset += 8
|
|
320
|
-
if offset + chunk_len > bufsize:
|
|
321
|
-
break
|
|
322
|
-
H.chunks[chunk_type] = payload[offset:offset + chunk_len]
|
|
323
|
-
offset += chunk_len
|
|
324
|
-
|
|
325
|
-
if CHUNK_CHANNEL_NAMES in H.chunks:
|
|
326
|
-
L = H.chunks[CHUNK_CHANNEL_NAMES].split(b'\0')
|
|
327
|
-
numLab = len(L)
|
|
328
|
-
if numLab >= H.nChannels:
|
|
329
|
-
H.labels = [x.decode('utf-8') for x in L[0:H.nChannels]]
|
|
330
|
-
|
|
331
|
-
return H
|
|
332
|
-
|
|
333
|
-
def putHeader(self, nChannels, fSample, dataType, labels=None,
|
|
334
|
-
chunks=None, reponse=True):
|
|
335
|
-
haveLabels = False
|
|
336
|
-
extras = b''
|
|
337
|
-
|
|
338
|
-
if (type(labels)==list) and (len(labels)==0):
|
|
339
|
-
labels=None
|
|
340
|
-
|
|
341
|
-
if not(labels is None):
|
|
342
|
-
serLabels = b''
|
|
343
|
-
for n in range(0, nChannels):
|
|
344
|
-
# ensure that labels are ascii strings, not unicode
|
|
345
|
-
serLabels += labels[n].encode('ascii', 'ignore') + b'\0'
|
|
346
|
-
try:
|
|
347
|
-
pass
|
|
348
|
-
except:
|
|
349
|
-
raise ValueError('Channels names (labels), if given,'
|
|
350
|
-
' must be a list of N=numChannels strings')
|
|
351
|
-
|
|
352
|
-
extras = struct.pack('II', CHUNK_CHANNEL_NAMES,
|
|
353
|
-
len(serLabels)) + serLabels
|
|
354
|
-
haveLabels = True
|
|
355
|
-
|
|
356
|
-
if not(chunks is None):
|
|
357
|
-
for chunk_type, chunk_data in chunks:
|
|
358
|
-
if haveLabels and chunk_type == CHUNK_CHANNEL_NAMES:
|
|
359
|
-
# ignore channel names chunk in case we got labels
|
|
360
|
-
continue
|
|
361
|
-
extras += struct.pack('II', chunk_type,
|
|
362
|
-
len(chunk_data)) + chunk_data
|
|
363
|
-
|
|
364
|
-
sizeChunks = len(extras)
|
|
365
|
-
|
|
366
|
-
if reponse:
|
|
367
|
-
command = PUT_HDR
|
|
368
|
-
else:
|
|
369
|
-
command = PUT_HDR_NORESPONSE
|
|
370
|
-
|
|
371
|
-
hdef = struct.pack('IIIfII', nChannels, 0, 0,
|
|
372
|
-
fSample, dataType, sizeChunks)
|
|
373
|
-
request = struct.pack('HHI', VERSION, command,
|
|
374
|
-
sizeChunks + len(hdef)) + hdef + extras
|
|
375
|
-
self.sendRaw(request)
|
|
376
|
-
|
|
377
|
-
if reponse:
|
|
378
|
-
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
379
|
-
if status != PUT_OK:
|
|
380
|
-
raise IOError('Header could not be written')
|
|
381
|
-
|
|
382
|
-
def getData(self, index=None):
|
|
383
|
-
"""
|
|
384
|
-
getData([indices]) -- retrieve data samples and return them as a
|
|
385
|
-
Numpy array, samples in rows(!). The 'indices' argument is optional,
|
|
386
|
-
and if given, must be a tuple or list with inclusive, zero-based
|
|
387
|
-
start/end indices.
|
|
388
|
-
"""
|
|
389
|
-
|
|
390
|
-
if index is None:
|
|
391
|
-
request = struct.pack('HHI', VERSION, GET_DAT, 0)
|
|
392
|
-
else:
|
|
393
|
-
indS = int(index[0])
|
|
394
|
-
indE = int(index[1])
|
|
395
|
-
request = struct.pack('HHIII', VERSION, GET_DAT, 8, indS, indE)
|
|
396
|
-
self.sendRaw(request)
|
|
397
|
-
|
|
398
|
-
(status, bufsize, payload) = self.receiveResponse()
|
|
399
|
-
if status == GET_ERR:
|
|
400
|
-
return None
|
|
401
|
-
|
|
402
|
-
if status != GET_OK:
|
|
403
|
-
self.disconnect()
|
|
404
|
-
raise IOError('Bad response from buffer server - disconnecting')
|
|
405
|
-
|
|
406
|
-
if bufsize < 16:
|
|
407
|
-
self.disconnect()
|
|
408
|
-
raise IOError('Invalid DATA packet received (too few bytes)')
|
|
409
|
-
|
|
410
|
-
(nchans, nsamp, datype, bfsiz) = struct.unpack('IIII', payload[0:16])
|
|
411
|
-
|
|
412
|
-
if bfsiz < bufsize - 16 or datype >= len(numpyType):
|
|
413
|
-
raise IOError('Invalid DATA packet received')
|
|
414
|
-
|
|
415
|
-
raw = payload[16:bfsiz + 16]
|
|
416
|
-
D = numpy.ndarray((nsamp, nchans), dtype=numpyType[datype], buffer=raw)
|
|
417
|
-
|
|
418
|
-
return D
|
|
419
|
-
|
|
420
|
-
def getEvents(self, index=None):
|
|
421
|
-
"""
|
|
422
|
-
getEvents([indices]) -- retrieve events and return them as a list
|
|
423
|
-
of Event objects. The 'indices' argument is optional, and if given,
|
|
424
|
-
must be a tuple or list with inclusive, zero-based start/end indices.
|
|
425
|
-
The 'type' and 'value' fields of the event will be converted to strings
|
|
426
|
-
or Numpy arrays.
|
|
427
|
-
"""
|
|
428
|
-
|
|
429
|
-
if index is None:
|
|
430
|
-
request = struct.pack('HHI', VERSION, GET_EVT, 0)
|
|
431
|
-
else:
|
|
432
|
-
indS = int(index[0])
|
|
433
|
-
indE = int(index[1])
|
|
434
|
-
request = struct.pack('HHIII', VERSION, GET_EVT, 8, indS, indE)
|
|
435
|
-
self.sendRaw(request)
|
|
436
|
-
|
|
437
|
-
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
438
|
-
if status == GET_ERR:
|
|
439
|
-
return []
|
|
440
|
-
|
|
441
|
-
if status != GET_OK:
|
|
442
|
-
self.disconnect()
|
|
443
|
-
raise IOError('Bad response from buffer server - disconnecting')
|
|
444
|
-
|
|
445
|
-
offset = 0
|
|
446
|
-
E = []
|
|
447
|
-
while 1:
|
|
448
|
-
e = Event()
|
|
449
|
-
nextOffset = e.deserialize(resp_buf[offset:])
|
|
450
|
-
if nextOffset == 0:
|
|
451
|
-
break
|
|
452
|
-
E.append(e)
|
|
453
|
-
offset = offset + nextOffset
|
|
454
|
-
|
|
455
|
-
return E
|
|
456
|
-
|
|
457
|
-
def putEvents(self, E, reponse=True):
|
|
458
|
-
"""
|
|
459
|
-
putEvents(E) -- writes a single or multiple events, depending on
|
|
460
|
-
whether an 'Event' object, or a list of 'Event' objects is
|
|
461
|
-
given as an argument.
|
|
462
|
-
"""
|
|
463
|
-
if isinstance(E, Event):
|
|
464
|
-
buf = E.serialize()
|
|
465
|
-
else:
|
|
466
|
-
buf = ''
|
|
467
|
-
num = 0
|
|
468
|
-
for e in E:
|
|
469
|
-
if not(isinstance(e, Event)):
|
|
470
|
-
raise 'Element %i in given list is not an Event' % num
|
|
471
|
-
buf = buf + e.serialize()
|
|
472
|
-
num = num + 1
|
|
473
|
-
|
|
474
|
-
if reponse:
|
|
475
|
-
command = PUT_EVT
|
|
476
|
-
else:
|
|
477
|
-
command = PUT_EVT_NORESPONSE
|
|
478
|
-
|
|
479
|
-
self.sendRequest(command, buf)
|
|
480
|
-
|
|
481
|
-
if reponse:
|
|
482
|
-
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
483
|
-
if status != PUT_OK:
|
|
484
|
-
raise IOError('Events could not be written.')
|
|
485
|
-
|
|
486
|
-
def putData(self, D, response=True):
|
|
487
|
-
"""
|
|
488
|
-
putData(D) -- writes samples that must be given as a NUMPY array,
|
|
489
|
-
samples x channels. The type of the samples (D) and the number of
|
|
490
|
-
channels must match the corresponding quantities in the FieldTrip
|
|
491
|
-
buffer.
|
|
492
|
-
"""
|
|
493
|
-
|
|
494
|
-
if not(isinstance(D, numpy.ndarray)) or len(D.shape) != 2:
|
|
495
|
-
raise ValueError(
|
|
496
|
-
'Data must be given as a NUMPY array (samples x channels)')
|
|
497
|
-
|
|
498
|
-
nSamp = D.shape[0]
|
|
499
|
-
nChan = D.shape[1]
|
|
500
|
-
|
|
501
|
-
(dataType, dataBuf) = serialize(D)
|
|
502
|
-
|
|
503
|
-
dataBufSize = len(dataBuf)
|
|
504
|
-
|
|
505
|
-
if response:
|
|
506
|
-
command = PUT_DAT
|
|
507
|
-
else:
|
|
508
|
-
command = PUT_DAT_NORESPONSE
|
|
509
|
-
|
|
510
|
-
request = struct.pack('HHI', VERSION, command, 16 + dataBufSize)
|
|
511
|
-
dataDef = struct.pack('IIII', nChan, nSamp, dataType, dataBufSize)
|
|
512
|
-
self.sendRaw(request + dataDef + dataBuf)
|
|
513
|
-
|
|
514
|
-
if response:
|
|
515
|
-
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
516
|
-
if status != PUT_OK:
|
|
517
|
-
raise IOError('Samples could not be written.')
|
|
518
|
-
|
|
519
|
-
def poll(self):
|
|
520
|
-
|
|
521
|
-
request = struct.pack('HHIIII', VERSION, WAIT_DAT, 12, 0, 0, 0)
|
|
522
|
-
self.sendRaw(request)
|
|
523
|
-
|
|
524
|
-
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
525
|
-
|
|
526
|
-
if status != WAIT_OK or bufsize < 8:
|
|
527
|
-
raise IOError('Polling failed.')
|
|
528
|
-
|
|
529
|
-
return struct.unpack('II', resp_buf[0:8])
|
|
530
|
-
|
|
531
|
-
def wait(self, nsamples, nevents, timeout):
|
|
532
|
-
request = struct.pack('HHIIII', VERSION, WAIT_DAT,
|
|
533
|
-
12, int(nsamples), int(nevents), int(timeout))
|
|
534
|
-
self.sendRaw(request)
|
|
535
|
-
|
|
536
|
-
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
537
|
-
|
|
538
|
-
if status != WAIT_OK or bufsize < 8:
|
|
539
|
-
raise IOError('Wait request failed.')
|
|
540
|
-
|
|
541
|
-
return struct.unpack('II', resp_buf[0:8])
|
|
542
|
-
|
|
543
|
-
if __name__ == "__main__":
|
|
544
|
-
# Just a small demo for testing purposes...
|
|
545
|
-
# This should be moved to a separate file at some point
|
|
546
|
-
import sys
|
|
547
|
-
|
|
548
|
-
hostname = 'localhost'
|
|
549
|
-
port = 1972
|
|
550
|
-
|
|
551
|
-
if len(sys.argv) > 1:
|
|
552
|
-
hostname = sys.argv[1]
|
|
553
|
-
if len(sys.argv) > 2:
|
|
554
|
-
try:
|
|
555
|
-
port = int(sys.argv[2])
|
|
556
|
-
except:
|
|
557
|
-
print(('Error: second argument (%s) must be a valid (=integer)'
|
|
558
|
-
' port number' % sys.argv[2]))
|
|
559
|
-
sys.exit(1)
|
|
560
|
-
|
|
561
|
-
ftc = Client()
|
|
562
|
-
|
|
563
|
-
print('Trying to connect to buffer on %s:%i ...' % (hostname, port))
|
|
564
|
-
ftc.connect(hostname, port)
|
|
565
|
-
|
|
566
|
-
print('\nConnected - trying to read header...')
|
|
567
|
-
H = ftc.getHeader()
|
|
568
|
-
|
|
569
|
-
if H is None:
|
|
570
|
-
print('Failed!')
|
|
571
|
-
else:
|
|
572
|
-
print(H)
|
|
573
|
-
print(H.labels)
|
|
574
|
-
|
|
575
|
-
if H.nSamples > 0:
|
|
576
|
-
print('\nTrying to read last sample...')
|
|
577
|
-
index = H.nSamples - 1
|
|
578
|
-
D = ftc.getData([index, index])
|
|
579
|
-
print(D)
|
|
580
|
-
|
|
581
|
-
if H.nEvents > 0:
|
|
582
|
-
print('\nTrying to read (all) events...')
|
|
583
|
-
E = ftc.getEvents()
|
|
584
|
-
for e in E:
|
|
585
|
-
print(e)
|
|
586
|
-
|
|
587
|
-
print(ftc.poll())
|
|
588
|
-
|
|
589
|
-
ftc.disconnect()
|
|
1
|
+
"""
|
|
2
|
+
FieldTrip buffer (V1) client in pure Python
|
|
3
|
+
(C) 2010 S. Klanke
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Obtained from https://github.com/fieldtrip/fieldtrip/blob/master/realtime/src/buffer/python/FieldTrip.py
|
|
7
|
+
|
|
8
|
+
# We need socket, struct, and numpy
|
|
9
|
+
import socket
|
|
10
|
+
import struct
|
|
11
|
+
import numpy
|
|
12
|
+
import unicodedata
|
|
13
|
+
|
|
14
|
+
VERSION = 1
|
|
15
|
+
|
|
16
|
+
PUT_HDR = 0x0101
|
|
17
|
+
PUT_DAT = 0x0102
|
|
18
|
+
PUT_EVT = 0x0103
|
|
19
|
+
PUT_OK = 0x0104
|
|
20
|
+
PUT_ERR = 0x0105
|
|
21
|
+
GET_HDR = 0x0201
|
|
22
|
+
GET_DAT = 0x0202
|
|
23
|
+
GET_EVT = 0x0203
|
|
24
|
+
GET_OK = 0x0204
|
|
25
|
+
GET_ERR = 0x0205
|
|
26
|
+
FLUSH_HDR = 0x0301
|
|
27
|
+
FLUSH_DAT = 0x0302
|
|
28
|
+
FLUSH_EVT = 0x0303
|
|
29
|
+
FLUSH_OK = 0x0304
|
|
30
|
+
FLUSH_ERR = 0x0305
|
|
31
|
+
WAIT_DAT = 0x0402
|
|
32
|
+
WAIT_OK = 0x0404
|
|
33
|
+
WAIT_ERR = 0x0405
|
|
34
|
+
PUT_HDR_NORESPONSE = 0x0501
|
|
35
|
+
PUT_DAT_NORESPONSE = 0x0502
|
|
36
|
+
PUT_EVT_NORESPONSE = 0x0503
|
|
37
|
+
|
|
38
|
+
DATATYPE_CHAR = 0
|
|
39
|
+
DATATYPE_UINT8 = 1
|
|
40
|
+
DATATYPE_UINT16 = 2
|
|
41
|
+
DATATYPE_UINT32 = 3
|
|
42
|
+
DATATYPE_UINT64 = 4
|
|
43
|
+
DATATYPE_INT8 = 5
|
|
44
|
+
DATATYPE_INT16 = 6
|
|
45
|
+
DATATYPE_INT32 = 7
|
|
46
|
+
DATATYPE_INT64 = 8
|
|
47
|
+
DATATYPE_FLOAT32 = 9
|
|
48
|
+
DATATYPE_FLOAT64 = 10
|
|
49
|
+
DATATYPE_UNKNOWN = 0xFFFFFFFF
|
|
50
|
+
|
|
51
|
+
CHUNK_UNSPECIFIED = 0
|
|
52
|
+
CHUNK_CHANNEL_NAMES = 1
|
|
53
|
+
CHUNK_CHANNEL_FLAGS = 2
|
|
54
|
+
CHUNK_RESOLUTIONS = 3
|
|
55
|
+
CHUNK_ASCII_KEYVAL = 4
|
|
56
|
+
CHUNK_NIFTI1 = 5
|
|
57
|
+
CHUNK_SIEMENS_AP = 6
|
|
58
|
+
CHUNK_CTF_RES4 = 7
|
|
59
|
+
CHUNK_NEUROMAG_FIF = 8
|
|
60
|
+
CHUNK_NEUROMAG_ISOTRAK = 9
|
|
61
|
+
CHUNK_NEUROMAG_HPIRESULT = 10
|
|
62
|
+
|
|
63
|
+
# List for converting FieldTrip datatypes to Numpy datatypes
|
|
64
|
+
numpyType = ['int8', 'uint8', 'uint16', 'uint32', 'uint64',
|
|
65
|
+
'int8', 'int16', 'int32', 'int64', 'float32', 'float64']
|
|
66
|
+
# Corresponding word sizes
|
|
67
|
+
wordSize = [1, 1, 2, 4, 8, 1, 2, 4, 8, 4, 8]
|
|
68
|
+
# FieldTrip data type as indexed by numpy dtype.num
|
|
69
|
+
# this goes 0 => nothing, 1..4 => int8, uint8, int16, uint16, 7..10 =>
|
|
70
|
+
# int32, uint32, int64, uint64 11..12 => float32, float64
|
|
71
|
+
dataType = [-1, 5, 1, 6, 2, -1, -1, 7, 3, 8, 4, 9, 10]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def serialize(A):
|
|
75
|
+
"""
|
|
76
|
+
Returns FieldTrip data type and string representation of the given
|
|
77
|
+
object, if possible.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(A, str):
|
|
80
|
+
return (0, A)
|
|
81
|
+
|
|
82
|
+
if isinstance(A, numpy.ndarray):
|
|
83
|
+
dt = A.dtype
|
|
84
|
+
if not(dt.isnative) or dt.num < 1 or dt.num >= len(dataType):
|
|
85
|
+
return (DATATYPE_UNKNOWN, None)
|
|
86
|
+
|
|
87
|
+
ft = dataType[dt.num]
|
|
88
|
+
if ft == -1:
|
|
89
|
+
return (DATATYPE_UNKNOWN, None)
|
|
90
|
+
|
|
91
|
+
if A.flags['C_CONTIGUOUS']:
|
|
92
|
+
# great, just use the array's buffer interface
|
|
93
|
+
return (ft, A.tostring())
|
|
94
|
+
|
|
95
|
+
# otherwise, we need a copy to C order
|
|
96
|
+
AC = A.copy('C')
|
|
97
|
+
return (ft, AC.tostring())
|
|
98
|
+
|
|
99
|
+
if isinstance(A, int):
|
|
100
|
+
return (DATATYPE_INT32, struct.pack('i', A))
|
|
101
|
+
|
|
102
|
+
if isinstance(A, float):
|
|
103
|
+
return (DATATYPE_FLOAT64, struct.pack('d', A))
|
|
104
|
+
|
|
105
|
+
return (DATATYPE_UNKNOWN, None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Chunk:
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self.type = 0
|
|
112
|
+
self.size = 0
|
|
113
|
+
self.buf = ''
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Header:
|
|
117
|
+
|
|
118
|
+
"""Class for storing header information in the FieldTrip buffer format"""
|
|
119
|
+
|
|
120
|
+
def __init__(self):
|
|
121
|
+
self.nChannels = 0
|
|
122
|
+
self.nSamples = 0
|
|
123
|
+
self.nEvents = 0
|
|
124
|
+
self.fSample = 0.0
|
|
125
|
+
self.dataType = 0
|
|
126
|
+
self.chunks = {}
|
|
127
|
+
self.labels = []
|
|
128
|
+
|
|
129
|
+
def __str__(self):
|
|
130
|
+
return ('Channels.: %i\nSamples..: %i\nEvents...: %i\nSampFreq.: '
|
|
131
|
+
'%f\nDataType.: %s\n'
|
|
132
|
+
% (self.nChannels, self.nSamples, self.nEvents,
|
|
133
|
+
self.fSample, numpyType[self.dataType]))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Event:
|
|
137
|
+
"""Class for storing events in the FieldTrip buffer format"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, S=None):
|
|
140
|
+
if S is None:
|
|
141
|
+
self.type = ''
|
|
142
|
+
self.value = ''
|
|
143
|
+
self.sample = 0
|
|
144
|
+
self.offset = 0
|
|
145
|
+
self.duration = 0
|
|
146
|
+
else:
|
|
147
|
+
self.deserialize(S)
|
|
148
|
+
|
|
149
|
+
def __str__(self):
|
|
150
|
+
return ('Type.....: %s\nValue....: %s\nSample...: %i\nOffset...: '
|
|
151
|
+
'%i\nDuration.: %i\n' % (str(self.type), str(self.value),
|
|
152
|
+
self.sample, self.offset,
|
|
153
|
+
self.duration))
|
|
154
|
+
|
|
155
|
+
def deserialize(self, buf):
|
|
156
|
+
bufsize = len(buf)
|
|
157
|
+
if bufsize < 32:
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
(type_type, type_numel, value_type, value_numel, sample,
|
|
161
|
+
offset, duration, bsiz) = struct.unpack('IIIIIiiI', buf[0:32])
|
|
162
|
+
|
|
163
|
+
self.sample = sample
|
|
164
|
+
self.offset = offset
|
|
165
|
+
self.duration = duration
|
|
166
|
+
|
|
167
|
+
st = type_numel * wordSize[type_type]
|
|
168
|
+
sv = value_numel * wordSize[value_type]
|
|
169
|
+
|
|
170
|
+
if bsiz + 32 > bufsize or st + sv > bsiz:
|
|
171
|
+
raise IOError(
|
|
172
|
+
'Invalid event definition -- does not fit in given buffer')
|
|
173
|
+
|
|
174
|
+
raw_type = buf[32:32 + st]
|
|
175
|
+
raw_value = buf[32 + st:32 + st + sv]
|
|
176
|
+
|
|
177
|
+
if type_type == 0:
|
|
178
|
+
self.type = raw_type
|
|
179
|
+
else:
|
|
180
|
+
self.type = numpy.ndarray(
|
|
181
|
+
(type_numel), dtype=numpyType[type_type], buffer=raw_type)
|
|
182
|
+
|
|
183
|
+
if value_type == 0:
|
|
184
|
+
self.value = raw_value
|
|
185
|
+
else:
|
|
186
|
+
self.value = numpy.ndarray(
|
|
187
|
+
(value_numel), dtype=numpyType[value_type], buffer=raw_value)
|
|
188
|
+
|
|
189
|
+
return bsiz + 32
|
|
190
|
+
|
|
191
|
+
def serialize(self):
|
|
192
|
+
"""
|
|
193
|
+
Returns the contents of this event as a string, ready to
|
|
194
|
+
send over the network, or None in case of conversion problems.
|
|
195
|
+
"""
|
|
196
|
+
type_type, type_buf = serialize(self.type)
|
|
197
|
+
if type_type == DATATYPE_UNKNOWN:
|
|
198
|
+
return None
|
|
199
|
+
type_size = len(type_buf)
|
|
200
|
+
type_numel = type_size / wordSize[type_type]
|
|
201
|
+
|
|
202
|
+
value_type, value_buf = serialize(self.value)
|
|
203
|
+
if value_type == DATATYPE_UNKNOWN:
|
|
204
|
+
return None
|
|
205
|
+
value_size = len(value_buf)
|
|
206
|
+
value_numel = value_size / wordSize[value_type]
|
|
207
|
+
|
|
208
|
+
bufsize = type_size + value_size
|
|
209
|
+
|
|
210
|
+
S = struct.pack('IIIIIiiI', type_type, type_numel, value_type,
|
|
211
|
+
value_numel, int(self.sample), int(self.offset),
|
|
212
|
+
int(self.duration), bufsize)
|
|
213
|
+
return S + type_buf + value_buf
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class Client:
|
|
217
|
+
|
|
218
|
+
"""Class for managing a client connection to a FieldTrip buffer."""
|
|
219
|
+
|
|
220
|
+
def __init__(self):
|
|
221
|
+
self.isConnected = False
|
|
222
|
+
self.sock = []
|
|
223
|
+
|
|
224
|
+
def connect(self, hostname, port=1972):
|
|
225
|
+
"""
|
|
226
|
+
connect(hostname [, port]) -- make a connection, default port is
|
|
227
|
+
1972.
|
|
228
|
+
"""
|
|
229
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
230
|
+
self.sock.connect((hostname, port))
|
|
231
|
+
self.sock.setblocking(True)
|
|
232
|
+
self.isConnected = True
|
|
233
|
+
|
|
234
|
+
def disconnect(self):
|
|
235
|
+
"""disconnect() -- close a connection."""
|
|
236
|
+
if self.isConnected:
|
|
237
|
+
self.sock.close()
|
|
238
|
+
self.sock = []
|
|
239
|
+
self.isConnected = False
|
|
240
|
+
|
|
241
|
+
def sendRaw(self, request):
|
|
242
|
+
"""Send all bytes of the string 'request' out to socket."""
|
|
243
|
+
if not(self.isConnected):
|
|
244
|
+
raise IOError('Not connected to FieldTrip buffer')
|
|
245
|
+
|
|
246
|
+
N = len(request)
|
|
247
|
+
nw = self.sock.send(request)
|
|
248
|
+
while nw < N:
|
|
249
|
+
nw += self.sock.send(request[nw:])
|
|
250
|
+
|
|
251
|
+
def sendRequest(self, command, payload=None):
|
|
252
|
+
if payload is None:
|
|
253
|
+
request = struct.pack('HHI', VERSION, command, 0)
|
|
254
|
+
else:
|
|
255
|
+
request = struct.pack(
|
|
256
|
+
'HHI', VERSION, command, len(payload)) + payload
|
|
257
|
+
self.sendRaw(request)
|
|
258
|
+
|
|
259
|
+
def receiveResponse(self, minBytes=0):
|
|
260
|
+
"""
|
|
261
|
+
Receive response from server on socket 's' and return it as
|
|
262
|
+
(status,bufsize,payload).
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
resp_hdr = self.sock.recv(8)
|
|
266
|
+
while len(resp_hdr) < 8:
|
|
267
|
+
resp_hdr += self.sock.recv(8 - len(resp_hdr))
|
|
268
|
+
|
|
269
|
+
(version, command, bufsize) = struct.unpack('HHI', resp_hdr)
|
|
270
|
+
|
|
271
|
+
if version != VERSION:
|
|
272
|
+
self.disconnect()
|
|
273
|
+
raise IOError('Bad response from buffer server - disconnecting')
|
|
274
|
+
|
|
275
|
+
if bufsize > 0:
|
|
276
|
+
payload = self.sock.recv(bufsize)
|
|
277
|
+
while len(payload) < bufsize:
|
|
278
|
+
payload += self.sock.recv(bufsize - len(payload))
|
|
279
|
+
else:
|
|
280
|
+
payload = None
|
|
281
|
+
return (command, bufsize, payload)
|
|
282
|
+
|
|
283
|
+
def getHeader(self):
|
|
284
|
+
"""
|
|
285
|
+
getHeader() -- grabs header information from the buffer an returns
|
|
286
|
+
it as a Header object.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
self.sendRequest(GET_HDR)
|
|
290
|
+
(status, bufsize, payload) = self.receiveResponse()
|
|
291
|
+
|
|
292
|
+
if status == GET_ERR:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
if status != GET_OK:
|
|
296
|
+
self.disconnect()
|
|
297
|
+
raise IOError('Bad response from buffer server - disconnecting')
|
|
298
|
+
|
|
299
|
+
if bufsize < 24:
|
|
300
|
+
self.disconnect()
|
|
301
|
+
raise IOError('Invalid HEADER packet received (too few bytes) - '
|
|
302
|
+
'disconnecting')
|
|
303
|
+
|
|
304
|
+
(nchans, nsamp, nevt, fsamp, dtype,
|
|
305
|
+
bfsiz) = struct.unpack('IIIfII', payload[0:24])
|
|
306
|
+
|
|
307
|
+
H = Header()
|
|
308
|
+
H.nChannels = nchans
|
|
309
|
+
H.nSamples = nsamp
|
|
310
|
+
H.nEvents = nevt
|
|
311
|
+
H.fSample = fsamp
|
|
312
|
+
H.dataType = dtype
|
|
313
|
+
|
|
314
|
+
if bfsiz > 0:
|
|
315
|
+
offset = 24
|
|
316
|
+
while offset + 8 < bufsize:
|
|
317
|
+
(chunk_type, chunk_len) = struct.unpack(
|
|
318
|
+
'II', payload[offset:offset + 8])
|
|
319
|
+
offset += 8
|
|
320
|
+
if offset + chunk_len > bufsize:
|
|
321
|
+
break
|
|
322
|
+
H.chunks[chunk_type] = payload[offset:offset + chunk_len]
|
|
323
|
+
offset += chunk_len
|
|
324
|
+
|
|
325
|
+
if CHUNK_CHANNEL_NAMES in H.chunks:
|
|
326
|
+
L = H.chunks[CHUNK_CHANNEL_NAMES].split(b'\0')
|
|
327
|
+
numLab = len(L)
|
|
328
|
+
if numLab >= H.nChannels:
|
|
329
|
+
H.labels = [x.decode('utf-8') for x in L[0:H.nChannels]]
|
|
330
|
+
|
|
331
|
+
return H
|
|
332
|
+
|
|
333
|
+
def putHeader(self, nChannels, fSample, dataType, labels=None,
|
|
334
|
+
chunks=None, reponse=True):
|
|
335
|
+
haveLabels = False
|
|
336
|
+
extras = b''
|
|
337
|
+
|
|
338
|
+
if (type(labels)==list) and (len(labels)==0):
|
|
339
|
+
labels=None
|
|
340
|
+
|
|
341
|
+
if not(labels is None):
|
|
342
|
+
serLabels = b''
|
|
343
|
+
for n in range(0, nChannels):
|
|
344
|
+
# ensure that labels are ascii strings, not unicode
|
|
345
|
+
serLabels += labels[n].encode('ascii', 'ignore') + b'\0'
|
|
346
|
+
try:
|
|
347
|
+
pass
|
|
348
|
+
except:
|
|
349
|
+
raise ValueError('Channels names (labels), if given,'
|
|
350
|
+
' must be a list of N=numChannels strings')
|
|
351
|
+
|
|
352
|
+
extras = struct.pack('II', CHUNK_CHANNEL_NAMES,
|
|
353
|
+
len(serLabels)) + serLabels
|
|
354
|
+
haveLabels = True
|
|
355
|
+
|
|
356
|
+
if not(chunks is None):
|
|
357
|
+
for chunk_type, chunk_data in chunks:
|
|
358
|
+
if haveLabels and chunk_type == CHUNK_CHANNEL_NAMES:
|
|
359
|
+
# ignore channel names chunk in case we got labels
|
|
360
|
+
continue
|
|
361
|
+
extras += struct.pack('II', chunk_type,
|
|
362
|
+
len(chunk_data)) + chunk_data
|
|
363
|
+
|
|
364
|
+
sizeChunks = len(extras)
|
|
365
|
+
|
|
366
|
+
if reponse:
|
|
367
|
+
command = PUT_HDR
|
|
368
|
+
else:
|
|
369
|
+
command = PUT_HDR_NORESPONSE
|
|
370
|
+
|
|
371
|
+
hdef = struct.pack('IIIfII', nChannels, 0, 0,
|
|
372
|
+
fSample, dataType, sizeChunks)
|
|
373
|
+
request = struct.pack('HHI', VERSION, command,
|
|
374
|
+
sizeChunks + len(hdef)) + hdef + extras
|
|
375
|
+
self.sendRaw(request)
|
|
376
|
+
|
|
377
|
+
if reponse:
|
|
378
|
+
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
379
|
+
if status != PUT_OK:
|
|
380
|
+
raise IOError('Header could not be written')
|
|
381
|
+
|
|
382
|
+
def getData(self, index=None):
|
|
383
|
+
"""
|
|
384
|
+
getData([indices]) -- retrieve data samples and return them as a
|
|
385
|
+
Numpy array, samples in rows(!). The 'indices' argument is optional,
|
|
386
|
+
and if given, must be a tuple or list with inclusive, zero-based
|
|
387
|
+
start/end indices.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
if index is None:
|
|
391
|
+
request = struct.pack('HHI', VERSION, GET_DAT, 0)
|
|
392
|
+
else:
|
|
393
|
+
indS = int(index[0])
|
|
394
|
+
indE = int(index[1])
|
|
395
|
+
request = struct.pack('HHIII', VERSION, GET_DAT, 8, indS, indE)
|
|
396
|
+
self.sendRaw(request)
|
|
397
|
+
|
|
398
|
+
(status, bufsize, payload) = self.receiveResponse()
|
|
399
|
+
if status == GET_ERR:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
if status != GET_OK:
|
|
403
|
+
self.disconnect()
|
|
404
|
+
raise IOError('Bad response from buffer server - disconnecting')
|
|
405
|
+
|
|
406
|
+
if bufsize < 16:
|
|
407
|
+
self.disconnect()
|
|
408
|
+
raise IOError('Invalid DATA packet received (too few bytes)')
|
|
409
|
+
|
|
410
|
+
(nchans, nsamp, datype, bfsiz) = struct.unpack('IIII', payload[0:16])
|
|
411
|
+
|
|
412
|
+
if bfsiz < bufsize - 16 or datype >= len(numpyType):
|
|
413
|
+
raise IOError('Invalid DATA packet received')
|
|
414
|
+
|
|
415
|
+
raw = payload[16:bfsiz + 16]
|
|
416
|
+
D = numpy.ndarray((nsamp, nchans), dtype=numpyType[datype], buffer=raw)
|
|
417
|
+
|
|
418
|
+
return D
|
|
419
|
+
|
|
420
|
+
def getEvents(self, index=None):
|
|
421
|
+
"""
|
|
422
|
+
getEvents([indices]) -- retrieve events and return them as a list
|
|
423
|
+
of Event objects. The 'indices' argument is optional, and if given,
|
|
424
|
+
must be a tuple or list with inclusive, zero-based start/end indices.
|
|
425
|
+
The 'type' and 'value' fields of the event will be converted to strings
|
|
426
|
+
or Numpy arrays.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
if index is None:
|
|
430
|
+
request = struct.pack('HHI', VERSION, GET_EVT, 0)
|
|
431
|
+
else:
|
|
432
|
+
indS = int(index[0])
|
|
433
|
+
indE = int(index[1])
|
|
434
|
+
request = struct.pack('HHIII', VERSION, GET_EVT, 8, indS, indE)
|
|
435
|
+
self.sendRaw(request)
|
|
436
|
+
|
|
437
|
+
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
438
|
+
if status == GET_ERR:
|
|
439
|
+
return []
|
|
440
|
+
|
|
441
|
+
if status != GET_OK:
|
|
442
|
+
self.disconnect()
|
|
443
|
+
raise IOError('Bad response from buffer server - disconnecting')
|
|
444
|
+
|
|
445
|
+
offset = 0
|
|
446
|
+
E = []
|
|
447
|
+
while 1:
|
|
448
|
+
e = Event()
|
|
449
|
+
nextOffset = e.deserialize(resp_buf[offset:])
|
|
450
|
+
if nextOffset == 0:
|
|
451
|
+
break
|
|
452
|
+
E.append(e)
|
|
453
|
+
offset = offset + nextOffset
|
|
454
|
+
|
|
455
|
+
return E
|
|
456
|
+
|
|
457
|
+
def putEvents(self, E, reponse=True):
|
|
458
|
+
"""
|
|
459
|
+
putEvents(E) -- writes a single or multiple events, depending on
|
|
460
|
+
whether an 'Event' object, or a list of 'Event' objects is
|
|
461
|
+
given as an argument.
|
|
462
|
+
"""
|
|
463
|
+
if isinstance(E, Event):
|
|
464
|
+
buf = E.serialize()
|
|
465
|
+
else:
|
|
466
|
+
buf = ''
|
|
467
|
+
num = 0
|
|
468
|
+
for e in E:
|
|
469
|
+
if not(isinstance(e, Event)):
|
|
470
|
+
raise 'Element %i in given list is not an Event' % num
|
|
471
|
+
buf = buf + e.serialize()
|
|
472
|
+
num = num + 1
|
|
473
|
+
|
|
474
|
+
if reponse:
|
|
475
|
+
command = PUT_EVT
|
|
476
|
+
else:
|
|
477
|
+
command = PUT_EVT_NORESPONSE
|
|
478
|
+
|
|
479
|
+
self.sendRequest(command, buf)
|
|
480
|
+
|
|
481
|
+
if reponse:
|
|
482
|
+
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
483
|
+
if status != PUT_OK:
|
|
484
|
+
raise IOError('Events could not be written.')
|
|
485
|
+
|
|
486
|
+
def putData(self, D, response=True):
|
|
487
|
+
"""
|
|
488
|
+
putData(D) -- writes samples that must be given as a NUMPY array,
|
|
489
|
+
samples x channels. The type of the samples (D) and the number of
|
|
490
|
+
channels must match the corresponding quantities in the FieldTrip
|
|
491
|
+
buffer.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
if not(isinstance(D, numpy.ndarray)) or len(D.shape) != 2:
|
|
495
|
+
raise ValueError(
|
|
496
|
+
'Data must be given as a NUMPY array (samples x channels)')
|
|
497
|
+
|
|
498
|
+
nSamp = D.shape[0]
|
|
499
|
+
nChan = D.shape[1]
|
|
500
|
+
|
|
501
|
+
(dataType, dataBuf) = serialize(D)
|
|
502
|
+
|
|
503
|
+
dataBufSize = len(dataBuf)
|
|
504
|
+
|
|
505
|
+
if response:
|
|
506
|
+
command = PUT_DAT
|
|
507
|
+
else:
|
|
508
|
+
command = PUT_DAT_NORESPONSE
|
|
509
|
+
|
|
510
|
+
request = struct.pack('HHI', VERSION, command, 16 + dataBufSize)
|
|
511
|
+
dataDef = struct.pack('IIII', nChan, nSamp, dataType, dataBufSize)
|
|
512
|
+
self.sendRaw(request + dataDef + dataBuf)
|
|
513
|
+
|
|
514
|
+
if response:
|
|
515
|
+
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
516
|
+
if status != PUT_OK:
|
|
517
|
+
raise IOError('Samples could not be written.')
|
|
518
|
+
|
|
519
|
+
def poll(self):
|
|
520
|
+
|
|
521
|
+
request = struct.pack('HHIIII', VERSION, WAIT_DAT, 12, 0, 0, 0)
|
|
522
|
+
self.sendRaw(request)
|
|
523
|
+
|
|
524
|
+
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
525
|
+
|
|
526
|
+
if status != WAIT_OK or bufsize < 8:
|
|
527
|
+
raise IOError('Polling failed.')
|
|
528
|
+
|
|
529
|
+
return struct.unpack('II', resp_buf[0:8])
|
|
530
|
+
|
|
531
|
+
def wait(self, nsamples, nevents, timeout):
|
|
532
|
+
request = struct.pack('HHIIII', VERSION, WAIT_DAT,
|
|
533
|
+
12, int(nsamples), int(nevents), int(timeout))
|
|
534
|
+
self.sendRaw(request)
|
|
535
|
+
|
|
536
|
+
(status, bufsize, resp_buf) = self.receiveResponse()
|
|
537
|
+
|
|
538
|
+
if status != WAIT_OK or bufsize < 8:
|
|
539
|
+
raise IOError('Wait request failed.')
|
|
540
|
+
|
|
541
|
+
return struct.unpack('II', resp_buf[0:8])
|
|
542
|
+
|
|
543
|
+
if __name__ == "__main__":
|
|
544
|
+
# Just a small demo for testing purposes...
|
|
545
|
+
# This should be moved to a separate file at some point
|
|
546
|
+
import sys
|
|
547
|
+
|
|
548
|
+
hostname = 'localhost'
|
|
549
|
+
port = 1972
|
|
550
|
+
|
|
551
|
+
if len(sys.argv) > 1:
|
|
552
|
+
hostname = sys.argv[1]
|
|
553
|
+
if len(sys.argv) > 2:
|
|
554
|
+
try:
|
|
555
|
+
port = int(sys.argv[2])
|
|
556
|
+
except:
|
|
557
|
+
print(('Error: second argument (%s) must be a valid (=integer)'
|
|
558
|
+
' port number' % sys.argv[2]))
|
|
559
|
+
sys.exit(1)
|
|
560
|
+
|
|
561
|
+
ftc = Client()
|
|
562
|
+
|
|
563
|
+
print('Trying to connect to buffer on %s:%i ...' % (hostname, port))
|
|
564
|
+
ftc.connect(hostname, port)
|
|
565
|
+
|
|
566
|
+
print('\nConnected - trying to read header...')
|
|
567
|
+
H = ftc.getHeader()
|
|
568
|
+
|
|
569
|
+
if H is None:
|
|
570
|
+
print('Failed!')
|
|
571
|
+
else:
|
|
572
|
+
print(H)
|
|
573
|
+
print(H.labels)
|
|
574
|
+
|
|
575
|
+
if H.nSamples > 0:
|
|
576
|
+
print('\nTrying to read last sample...')
|
|
577
|
+
index = H.nSamples - 1
|
|
578
|
+
D = ftc.getData([index, index])
|
|
579
|
+
print(D)
|
|
580
|
+
|
|
581
|
+
if H.nEvents > 0:
|
|
582
|
+
print('\nTrying to read (all) events...')
|
|
583
|
+
E = ftc.getEvents()
|
|
584
|
+
for e in E:
|
|
585
|
+
print(e)
|
|
586
|
+
|
|
587
|
+
print(ftc.poll())
|
|
588
|
+
|
|
589
|
+
ftc.disconnect()
|