cerelink 8.0.1__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
cerelink/cbpy.pyx ADDED
@@ -0,0 +1,1157 @@
1
+ # distutils: language = c++
2
+ # cython: language_level=2
3
+ from cbsdk_cython cimport *
4
+ from libcpp cimport bool
5
+ from libc.stdlib cimport malloc, free
6
+ from libc.string cimport strncpy
7
+ cimport numpy as cnp
8
+ cimport cython
9
+
10
+ import sys
11
+ import locale
12
+ import typing
13
+
14
+ import numpy as np
15
+
16
+ # Initialize numpy C API
17
+ cnp.import_array()
18
+
19
+ # NumPy array flags for ownership
20
+ cdef extern from "numpy/arrayobject.h":
21
+ cdef int NPY_ARRAY_OWNDATA
22
+
23
+
24
+ # Determine the correct numpy dtype for PROCTIME based on its size
25
+ cdef object _proctime_dtype():
26
+ if sizeof(PROCTIME) == 4:
27
+ return np.uint32
28
+ elif sizeof(PROCTIME) == 8:
29
+ return np.uint64
30
+ else:
31
+ raise RuntimeError("Unexpected PROCTIME size: %d" % sizeof(PROCTIME))
32
+
33
+ # Cache the dtype for performance
34
+ _PROCTIME_DTYPE = _proctime_dtype()
35
+
36
+
37
+ def version(int instance=0):
38
+ """Get library version
39
+ Inputs:
40
+ instance - (optional) library instance number
41
+ Outputs:"
42
+ dictionary with following keys
43
+ major - major API version
44
+ minor - minor API version
45
+ release - release API version
46
+ beta - beta API version (0 if a non-beta)
47
+ protocol_major - major protocol version
48
+ protocol_minor - minor protocol version
49
+ nsp_major - major NSP firmware version
50
+ nsp_minor - minor NSP firmware version
51
+ nsp_release - release NSP firmware version
52
+ nsp_beta - beta NSP firmware version (0 if non-beta))
53
+ nsp_protocol_major - major NSP protocol version
54
+ nsp_protocol_minor - minor NSP protocol version
55
+ """
56
+
57
+ cdef cbSdkResult res
58
+ cdef cbSdkVersion ver
59
+
60
+ res = cbSdkGetVersion(<uint32_t>instance, &ver)
61
+ handle_result(res)
62
+
63
+ ver_dict = {'major':ver.major, 'minor':ver.minor, 'release':ver.release, 'beta':ver.beta,
64
+ 'protocol_major':ver.majorp, 'protocol_minor':ver.majorp,
65
+ 'nsp_major':ver.nspmajor, 'nsp_minor':ver.nspminor, 'nsp_release':ver.nsprelease, 'nsp_beta':ver.nspbeta,
66
+ 'nsp_protocol_major':ver.nspmajorp, 'nsp_protocol_minor':ver.nspmajorp
67
+ }
68
+ return <int>res, ver_dict
69
+
70
+
71
+ def defaultConParams():
72
+ #Note: Defaulting to 255.255.255.255 assumes the client is connected to the NSP via a switch.
73
+ #A direct connection might require the client-addr to be "192.168.137.1"
74
+ con_parms = {
75
+ 'client-addr': str(cbNET_UDP_ADDR_BCAST.decode("utf-8"))\
76
+ if ('linux' in sys.platform or 'linux2' in sys.platform) else '255.255.255.255',
77
+ 'client-port': cbNET_UDP_PORT_BCAST,
78
+ 'inst-addr': cbNET_UDP_ADDR_CNT.decode("utf-8"),
79
+ 'inst-port': cbNET_UDP_PORT_CNT,
80
+ 'receive-buffer-size': (8 * 1024 * 1024) if sys.platform == 'win32' else (6 * 1024 * 1024)
81
+ }
82
+ return con_parms
83
+
84
+
85
+ def open(int instance=0, connection: str = 'default', parameter: dict[str, str | int] | None = None) -> dict[str, str]:
86
+ """Open library.
87
+ Inputs:
88
+ connection - connection type, string can be one of the following
89
+ 'default': tries slave then master connection
90
+ 'master': tries master connection (UDP)
91
+ 'slave': tries slave connection (needs another master already open)
92
+ parameter - dictionary with following keys (all optional)
93
+ 'inst-addr': instrument IPv4 address.
94
+ 'inst-port': instrument port number.
95
+ 'client-addr': client IPv4 address.
96
+ 'client-port': client port number.
97
+ 'receive-buffer-size': override default network buffer size (low value may result in drops).
98
+ instance - (optional) library instance number
99
+ Outputs:
100
+ Same as "get_connection_type" command output
101
+
102
+ Test with:
103
+ from cerelink import cbpy
104
+ cbpy.open(parameter=cbpy.defaultConParams())
105
+ """
106
+ cdef cbSdkResult res
107
+
108
+ wconType = {'default': CBSDKCONNECTION_DEFAULT, 'slave': CBSDKCONNECTION_CENTRAL, 'master': CBSDKCONNECTION_UDP}
109
+ if not connection in wconType.keys():
110
+ raise RuntimeError("invalid connection %s" % connection)
111
+
112
+ cdef cbSdkConnectionType conType = wconType[connection]
113
+ cdef cbSdkConnection con
114
+
115
+ parameter = parameter if parameter is not None else {}
116
+ cdef bytes szOutIP = parameter.get('inst-addr', cbNET_UDP_ADDR_CNT.decode("utf-8")).encode()
117
+ cdef bytes szInIP = parameter.get('client-addr', '').encode()
118
+
119
+ con.szOutIP = szOutIP
120
+ con.nOutPort = parameter.get('inst-port', cbNET_UDP_PORT_CNT)
121
+ con.szInIP = szInIP
122
+ con.nInPort = parameter.get('client-port', cbNET_UDP_PORT_BCAST)
123
+ con.nRecBufSize = parameter.get('receive-buffer-size', 0)
124
+
125
+ res = cbSdkOpen(<uint32_t>instance, conType, con)
126
+
127
+ handle_result(res)
128
+
129
+ return get_connection_type(instance=instance)
130
+
131
+
132
+ def close(int instance=0):
133
+ """Close library.
134
+ Inputs:
135
+ instance - (optional) library instance number
136
+ """
137
+ return handle_result(cbSdkClose(<uint32_t>instance))
138
+
139
+
140
+ def get_connection_type(int instance=0) -> dict[str, str]:
141
+ """ Get connection type
142
+ Inputs:
143
+ instance - (optional) library instance number
144
+ Outputs:
145
+ dictionary with following keys
146
+ 'connection': Final established connection; can be any of:
147
+ 'Default', 'Slave', 'Master', 'Closed', 'Unknown'
148
+ 'instrument': Instrument connected to; can be any of:
149
+ 'NSP', 'nPlay', 'Local NSP', 'Remote nPlay', 'Unknown')
150
+ """
151
+
152
+ cdef cbSdkResult res
153
+ cdef cbSdkConnectionType conType
154
+ cdef cbSdkInstrumentType instType
155
+
156
+ res = cbSdkGetType(<uint32_t>instance, &conType, &instType)
157
+
158
+ handle_result(res)
159
+
160
+ connections = ["Default", "Slave", "Master", "Closed", "Unknown"]
161
+ instruments = ["NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"]
162
+
163
+ con_idx = conType
164
+ if con_idx < 0 or con_idx >= len(connections):
165
+ con_idx = len(connections) - 1
166
+ inst_idx = instType
167
+ if inst_idx < 0 or inst_idx >= len(instruments):
168
+ inst_idx = len(instruments) - 1
169
+
170
+ return {"connection": connections[con_idx], "instrument": instruments[inst_idx]}
171
+
172
+
173
+ def trial_config(
174
+ int instance=0,
175
+ activate=True,
176
+ n_continuous: int = 0,
177
+ n_event: int = 0,
178
+ n_comment: int = 0,
179
+ n_tracking: int = 0,
180
+ begin_channel: int = 0,
181
+ begin_mask: int = 0,
182
+ begin_value: int = 0,
183
+ end_channel: int = 0,
184
+ end_mask: int = 0,
185
+ end_value: int = 0,
186
+ ) -> None:
187
+ """
188
+ Configure trial settings. See cbSdkSetTrialConfig in cbsdk.h for details.
189
+
190
+ Inputs:
191
+ activate - boolean, set True to flush data cache and start collecting data immediately,
192
+ set False to stop collecting data immediately or, if not already in a trial,
193
+ to rely on the begin/end channel mask|value to start/stop collecting data.
194
+ n_continuous - integer, number of continuous data samples to be cached.
195
+ 0 means no continuous data is cached. -1 will use cbSdk_CONTINUOUS_DATA_SAMPLES.
196
+ n_event - integer, number of events to be cached.
197
+ 0 means no events are captured. -1 will use cbSdk_EVENT_DATA_SAMPLES.
198
+ n_comment - integer, number of comments to be cached.
199
+ n_tracking - integer, number of video tracking events to be cached. Deprecated.
200
+ begin_channel - integer, channel to monitor to trigger trial start
201
+ Ignored if activate is True.
202
+ begin_mask - integer, mask to bitwise-and with data on begin_channel to look for trigger
203
+ Ignored if activate is True.
204
+ begin_value - value to trigger trial start
205
+ Ignored if activate is True.
206
+ end_channel - integer, channel to monitor to trigger trial stop
207
+ Ignored if activate is True.
208
+ end_mask - mask to bitwise-and with data on end_channel to evaluate trigger
209
+ Ignored if activate is True.
210
+ end_value - value to trigger trial stop
211
+ Ignored if activate is True.
212
+ instance - (optional) library instance number
213
+ """
214
+
215
+ cdef cbSdkResult res
216
+ cdef cbSdkConfigParam cfg_param
217
+
218
+ # retrieve old values
219
+ res = cbsdk_get_trial_config(<uint32_t>instance, &cfg_param)
220
+ handle_result(res)
221
+
222
+ cfg_param.bActive = <uint32_t>activate
223
+
224
+ # Fill cfg_param with provided buffer_parameter values or default.
225
+ cfg_param.uWaveforms = 0 # waveforms do not work
226
+ cfg_param.uConts = n_continuous if n_continuous >= 0 else cbSdk_CONTINUOUS_DATA_SAMPLES
227
+ cfg_param.uEvents = n_event if n_event >= 0 else cbSdk_EVENT_DATA_SAMPLES
228
+ cfg_param.uComments = n_comment or 0
229
+ cfg_param.uTrackings = n_tracking or 0
230
+
231
+ # Fill cfg_param mask-related parameters with provided range_parameter or default.
232
+ cfg_param.Begchan = begin_channel
233
+ cfg_param.Begmask = begin_mask
234
+ cfg_param.Begval = begin_value
235
+ cfg_param.Endchan = end_channel
236
+ cfg_param.Endmask = end_mask
237
+ cfg_param.Endval = end_value
238
+
239
+ res = cbsdk_set_trial_config(<int>instance, &cfg_param)
240
+
241
+ handle_result(res)
242
+
243
+
244
+ def trial_event(int instance=0, bool seek=False, bool reset_clock=False):
245
+ """ Trial spike and event data.
246
+ Inputs:
247
+ instance - (optional) library instance number
248
+ seek - (optional) boolean
249
+ set False (default) to leave buffer intact -- peek only.
250
+ set True to clear all the data after it has been retrieved.
251
+ reset_clock - (optional) boolean
252
+ set False (default) to leave the trial clock alone.
253
+ set True to update the _next_ trial time to the current time.
254
+ This is overly complicated. Leave this as `False` unless you really know what you are doing.
255
+ Note: All timestamps are now in absolute device time.
256
+ Outputs:
257
+ list of arrays [channel, {'timestamps':[unit0_ts, ..., unitN_ts], 'events':digital_events}]
258
+ channel: integer, channel number (1-based)
259
+ digital_events: array, digital event values for channel (if a digital or serial channel)
260
+ unitN_ts: array, spike timestamps of unit N for channel (if an electrode channel));
261
+ """
262
+
263
+ cdef cbSdkResult res
264
+ cdef cbSdkTrialEvent trialevent
265
+
266
+ # get how many samples are available
267
+ res = cbsdk_init_trial_event(<uint32_t>instance, <int>reset_clock, &trialevent)
268
+ handle_result(res)
269
+
270
+ if trialevent.num_events == 0:
271
+ return res, {'timestamps': np.array([], dtype=_PROCTIME_DTYPE),
272
+ 'channels': np.array([], dtype=np.uint16),
273
+ 'units': np.array([], dtype=np.uint16)}
274
+
275
+ # Allocate flat arrays for event data
276
+ cdef cnp.ndarray timestamps = np.zeros(trialevent.num_events, dtype=_PROCTIME_DTYPE)
277
+ cdef cnp.ndarray channels = np.zeros(trialevent.num_events, dtype=np.uint16)
278
+ cdef cnp.ndarray units = np.zeros(trialevent.num_events, dtype=np.uint16)
279
+
280
+ # Point C structure to our numpy arrays
281
+ trialevent.timestamps = <PROCTIME *>cnp.PyArray_DATA(timestamps)
282
+ trialevent.channels = <uint16_t *>cnp.PyArray_DATA(channels)
283
+ trialevent.units = <uint16_t *>cnp.PyArray_DATA(units)
284
+ trialevent.waveforms = NULL # TODO: Add waveform support if needed
285
+
286
+ # get the trial
287
+ res = cbsdk_get_trial_event(<uint32_t>instance, <int>seek, &trialevent)
288
+ handle_result(res)
289
+
290
+ trial = {
291
+ 'timestamps': timestamps,
292
+ 'channels': channels,
293
+ 'units': units
294
+ }
295
+
296
+ return <int>res, trial
297
+
298
+
299
+ def trial_continuous(
300
+ int instance=0,
301
+ uint8_t group=0,
302
+ bool seek=True,
303
+ bool reset_clock=False,
304
+ timestamps=None,
305
+ samples=None,
306
+ uint32_t num_samples=0
307
+ ) -> dict[str, typing.Any]:
308
+ """
309
+ Trial continuous data for a specific sample group.
310
+
311
+ Inputs:
312
+ instance - (optional) library instance number
313
+ group - (optional) sample group to retrieve (0-6), default=0 will always return an empty result.
314
+ seek - (optional) boolean
315
+ set True (default) to advance read pointer after retrieval.
316
+ set False to peek -- data remains in buffer for next call.
317
+ reset_clock - (optional) boolean
318
+ Keep False (default) unless you really know what you are doing.
319
+ timestamps - (optional) pre-allocated numpy array for timestamps, shape=[num_samples], dtype=uint32 or uint64
320
+ If None, function will allocate
321
+ samples - (optional) pre-allocated numpy array for samples, shape=[num_samples, num_channels], dtype=int16
322
+ If None, function will allocate
323
+ num_samples - (optional) maximum samples to read. If 0 and arrays provided, inferred from array shape.
324
+ If arrays provided and this is specified, reads min(num_samples, array_size)
325
+
326
+ Outputs:
327
+ data - dictionary with keys:
328
+ 'group': group number (0-6)
329
+ 'count': number of channels in this group
330
+ 'chan': array of channel IDs (1-based)
331
+ 'num_samples': actual number of samples read
332
+ 'trial_start_time': PROCTIME timestamp when the current **trial** started
333
+ 'timestamps': numpy array [num_samples] of PROCTIME timestamps (absolute device time)
334
+ 'samples': numpy array [num_samples, count] of int16 sample data
335
+ """
336
+ cdef cbSdkResult res
337
+ cdef cbSdkTrialCont trialcont
338
+ cdef cnp.ndarray timestamps_array
339
+ cdef cnp.ndarray samples_array
340
+ cdef bool user_provided_arrays = timestamps is not None and samples is not None
341
+
342
+ # Set the group we want to retrieve
343
+ trialcont.group = group
344
+
345
+ # Initialize - get channel count and buffer info for this group
346
+ res = cbSdkInitTrialData(<uint32_t>instance, reset_clock, NULL, &trialcont, NULL, NULL, 0)
347
+ handle_result(res)
348
+
349
+ if trialcont.count == 0:
350
+ # No channels in this group
351
+ return {
352
+ "group": group,
353
+ "count": 0,
354
+ "chan": np.array([], dtype=np.uint16),
355
+ "num_samples": 0,
356
+ "trial_start_time": trialcont.trial_start_time,
357
+ "timestamps": np.array([], dtype=_PROCTIME_DTYPE),
358
+ "samples": np.array([], dtype=np.int16).reshape(0, 0)
359
+ }
360
+
361
+ # Determine num_samples to request
362
+ if user_provided_arrays:
363
+ # Validate and use user arrays
364
+ if not isinstance(timestamps, np.ndarray) or not isinstance(samples, np.ndarray):
365
+ raise ValueError("timestamps and samples must both be numpy arrays")
366
+
367
+ # Check dtypes
368
+ if timestamps.dtype not in [np.uint32, np.uint64]:
369
+ raise ValueError(f"timestamps must be dtype uint32 or uint64, got {timestamps.dtype}")
370
+ if samples.dtype != np.int16:
371
+ raise ValueError(f"samples must be dtype int16, got {samples.dtype}")
372
+
373
+ # Check contiguity
374
+ if not timestamps.flags['C_CONTIGUOUS']:
375
+ raise ValueError("timestamps array must be C-contiguous")
376
+ if not samples.flags['C_CONTIGUOUS']:
377
+ raise ValueError("samples array must be C-contiguous")
378
+
379
+ # Check shapes
380
+ if timestamps.ndim != 1:
381
+ raise ValueError(f"timestamps must be 1D array, got shape {timestamps.shape}")
382
+ if samples.ndim != 2:
383
+ raise ValueError(f"samples must be 2D array, got shape {samples.shape}")
384
+
385
+ if timestamps.shape[0] != samples.shape[0]:
386
+ raise ValueError(f"timestamps and samples must have same length: {timestamps.shape[0]} != {samples.shape[0]}")
387
+
388
+ if samples.shape[1] != trialcont.count:
389
+ raise ValueError(f"samples shape[1] must match channel count: {samples.shape[1]} != {trialcont.count} expected")
390
+
391
+ # Determine num_samples from arrays if not specified
392
+ if num_samples == 0:
393
+ num_samples = timestamps.shape[0]
394
+ else:
395
+ # Use minimum of requested and available array size
396
+ num_samples = min(num_samples, <uint32_t>timestamps.shape[0])
397
+
398
+ timestamps_array = timestamps
399
+ samples_array = samples
400
+ else:
401
+ # Allocate arrays
402
+ if num_samples == 0:
403
+ num_samples = trialcont.num_samples # Use what Init reported as available
404
+
405
+ timestamps_array = np.empty(num_samples, dtype=_PROCTIME_DTYPE)
406
+ samples_array = np.empty((num_samples, trialcont.count), dtype=np.int16)
407
+
408
+ # Set num_samples and point to array data
409
+ trialcont.num_samples = num_samples
410
+ trialcont.timestamps = <PROCTIME*>cnp.PyArray_DATA(timestamps_array)
411
+ trialcont.samples = <int16_t*>cnp.PyArray_DATA(samples_array)
412
+
413
+ # Get the data
414
+ res = cbSdkGetTrialData(<uint32_t>instance, <uint32_t>seek, NULL, &trialcont, NULL, NULL)
415
+ handle_result(res)
416
+
417
+ # Build channel array
418
+ cdef cnp.ndarray chan_array = np.empty(trialcont.count, dtype=np.uint16)
419
+ cdef uint16_t[::1] chan_view = chan_array
420
+ cdef int i
421
+ for i in range(trialcont.count):
422
+ chan_view[i] = trialcont.chan[i]
423
+
424
+ # Prepare output - actual num_samples may be less than requested
425
+ cdef uint32_t actual_samples = trialcont.num_samples
426
+
427
+ return {
428
+ 'group': group,
429
+ 'count': trialcont.count,
430
+ 'chan': chan_array,
431
+ 'num_samples': actual_samples,
432
+ 'trial_start_time': trialcont.trial_start_time,
433
+ 'timestamps': timestamps_array[:actual_samples],
434
+ 'samples': samples_array[:actual_samples, :]
435
+ }
436
+
437
+
438
+ def trial_data(int instance=0, bool seek=False, bool reset_clock=False,
439
+ bool do_event=True, bool do_cont=True, bool do_comment=False, unsigned long wait_for_comment_msec=250):
440
+ """
441
+
442
+ :param instance: (optional) library instance number
443
+ :param seek: (optional) boolean
444
+ set False (default) to peek only and leave buffer intact.
445
+ set True to advance the data pointer after retrieval.
446
+ :param reset_clock - (optional) boolean
447
+ set False (default) to leave the trial clock alone.
448
+ set True to update the _next_ trial time to the current time.
449
+ This is overly complicated. Leave this as `False` unless you really know what you are doing.
450
+ Note: All timestamps are now in absolute device time.
451
+ :param do_event: (optional) boolean. Set False to skip fetching events.
452
+ :param do_cont: (optional) boolean. Set False to skip fetching continuous data.
453
+ :param do_comment: (optional) boolean. Set to True to fetch comments.
454
+ :param wait_for_comment_msec: (optional) unsigned long. How long we should wait for new comments.
455
+ Default (0) will not wait and will only return comments that existed prior to calling this.
456
+ :return: (result, event_data, continuous_data, t_zero, comment_data)
457
+ res: (int) returned by cbsdk
458
+ event data: list of arrays [channel, {'timestamps':[unit0_ts, ..., unitN_ts], 'events':digital_events}]
459
+ channel: integer, channel number (1-based)
460
+ digital_events: array, digital event values for channel (if a digital or serial channel)
461
+ unitN_ts: array, spike timestamps of unit N for channel (if an electrode channel)
462
+ Note: timestamps are PROCTIME (uint32 or uint64), events are uint16
463
+ continuous data: list of dictionaries, one per sample group (0-7) that has channels:
464
+ {
465
+ 'group': group number (0-7),
466
+ 'count': number of channels,
467
+ 'chan': numpy array of channel IDs (1-based),
468
+ 'sample_rate': sample rate (Hz),
469
+ 'num_samples': number of samples,
470
+ 'timestamps': numpy array [num_samples] of PROCTIME,
471
+ 'samples': numpy array [num_samples, count] of int16
472
+ }
473
+ t_zero: deprecated, always 0
474
+ comment_data: list of lists the form [timestamp, comment_str, charset, rgba]
475
+ timestamp: PROCTIME
476
+ comment_str: the comment in binary.
477
+ Use comment_str.decode('utf-16' if charset==1 else locale.getpreferredencoding())
478
+ rgba: integer; the comment colour. 8 bits each for r, g, b, a
479
+ """
480
+
481
+ cdef cbSdkResult res
482
+ cdef cbSdkTrialEvent trialevent
483
+ cdef cbSdkTrialComment trialcomm
484
+ cdef cbSdkTrialCont trialcont_group
485
+ # cdef uint8_t ch_type
486
+ cdef uint32_t b_dig_in
487
+ cdef uint32_t b_serial
488
+
489
+ cdef uint32_t tzero = 0
490
+ cdef int comm_ix
491
+ cdef uint32_t num_samples
492
+ cdef int channel
493
+ cdef uint16_t ch
494
+ cdef int u
495
+ cdef int g, j
496
+ cdef uint32_t actual_samples_grp
497
+
498
+ cdef cnp.ndarray mxa_proctime
499
+ cdef cnp.uint16_t[:] mxa_u16
500
+ cdef cnp.uint8_t[:] mxa_u8
501
+ cdef cnp.uint32_t[:] mxa_u32
502
+ cdef cnp.ndarray timestamps_grp
503
+ cdef cnp.ndarray samples_grp
504
+ cdef cnp.ndarray chan_array_grp
505
+ cdef uint16_t[::1] chan_view_grp
506
+
507
+ trial_event = []
508
+ trial_cont = []
509
+ trial_comment = []
510
+
511
+ # get how many samples are available
512
+ # Note: continuous data is now handled per-group below, so we don't init it here
513
+ res = cbsdk_init_trial_data(<uint32_t>instance, <int>reset_clock, &trialevent if do_event else NULL,
514
+ NULL, &trialcomm if do_comment else NULL,
515
+ wait_for_comment_msec)
516
+ handle_result(res)
517
+
518
+ # Early return if none of the requested data are available.
519
+ # For continuous, we'll check per-group below
520
+ if (not do_event or (trialevent.num_events == 0)) \
521
+ and (not do_comment or (trialcomm.num_samples == 0)) \
522
+ and (not do_cont):
523
+ return res, trial_event, trial_cont, tzero, trial_comment
524
+
525
+ # Events #
526
+ # ------ #
527
+ if do_event and trialevent.num_events > 0:
528
+ # Allocate flat arrays for event data
529
+ timestamps = np.zeros(trialevent.num_events, dtype=_PROCTIME_DTYPE)
530
+ channels = np.zeros(trialevent.num_events, dtype=np.uint16)
531
+ units = np.zeros(trialevent.num_events, dtype=np.uint16)
532
+
533
+ # Point C structure to our numpy arrays
534
+ trialevent.timestamps = <PROCTIME *>cnp.PyArray_DATA(timestamps)
535
+ trialevent.channels = <uint16_t *>cnp.PyArray_DATA(channels)
536
+ trialevent.units = <uint16_t *>cnp.PyArray_DATA(units)
537
+ trialevent.waveforms = NULL # TODO: Add waveform support if needed
538
+
539
+ trial_event = {
540
+ 'timestamps': timestamps,
541
+ 'channels': channels,
542
+ 'units': units
543
+ }
544
+
545
+ # Continuous #
546
+ # ---------- #
547
+ # With the new group-based API, we fetch each group (0-7) separately
548
+ if do_cont:
549
+ # Fetch all 8 groups
550
+ for g in range(8): # Groups 0-7 (cbMAXGROUPS)
551
+ trialcont_group.group = g
552
+
553
+ # Initialize this group
554
+ res = cbSdkInitTrialData(<uint32_t>instance, reset_clock, NULL, &trialcont_group, NULL, NULL, 0)
555
+ if res != CBSDKRESULT_SUCCESS or trialcont_group.count == 0:
556
+ continue # Skip groups with no channels
557
+
558
+ # Allocate arrays for this group
559
+ num_samples = trialcont_group.num_samples
560
+ timestamps_grp = np.empty(num_samples, dtype=_PROCTIME_DTYPE)
561
+ samples_grp = np.empty((num_samples, trialcont_group.count), dtype=np.int16)
562
+
563
+ # Point trialcont to arrays
564
+ trialcont_group.num_samples = num_samples
565
+ trialcont_group.timestamps = <PROCTIME*>cnp.PyArray_DATA(timestamps_grp)
566
+ trialcont_group.samples = <int16_t*>cnp.PyArray_DATA(samples_grp)
567
+
568
+ # Get data for this group
569
+ res = cbSdkGetTrialData(<uint32_t>instance, 0, NULL, &trialcont_group, NULL, NULL)
570
+ if res != CBSDKRESULT_SUCCESS:
571
+ continue
572
+
573
+ # Build channel array
574
+ chan_array_grp = np.empty(trialcont_group.count, dtype=np.uint16)
575
+ chan_view_grp = chan_array_grp
576
+ for j in range(trialcont_group.count):
577
+ chan_view_grp[j] = trialcont_group.chan[j]
578
+
579
+ actual_samples_grp = trialcont_group.num_samples
580
+
581
+ # Append group data as dictionary
582
+ trial_cont.append({
583
+ 'group': g,
584
+ 'count': trialcont_group.count,
585
+ 'chan': chan_array_grp,
586
+ 'sample_rate': trialcont_group.sample_rate,
587
+ 'num_samples': actual_samples_grp,
588
+ 'timestamps': timestamps_grp[:actual_samples_grp],
589
+ 'samples': samples_grp[:actual_samples_grp, :]
590
+ })
591
+
592
+ # Comments #
593
+ # -------- #
594
+ if do_comment and (trialcomm.num_samples > 0):
595
+ # Allocate memory
596
+ # For charsets;
597
+ mxa_u8 = np.zeros(trialcomm.num_samples, dtype=np.uint8)
598
+ trialcomm.charsets = <uint8_t *>&mxa_u8[0]
599
+ my_charsets = np.asarray(mxa_u8)
600
+ # For rgbas
601
+ mxa_u32 = np.zeros(trialcomm.num_samples, dtype=np.uint32)
602
+ trialcomm.rgbas = <uint32_t *>&mxa_u32[0]
603
+ my_rgbas = np.asarray(mxa_u32)
604
+ # For timestamps
605
+ mxa_proctime = np.zeros(trialcomm.num_samples, dtype=_PROCTIME_DTYPE)
606
+ trialcomm.timestamps = <PROCTIME *>cnp.PyArray_DATA(mxa_proctime)
607
+ my_timestamps = mxa_proctime
608
+ # For comments
609
+ trialcomm.comments = <uint8_t **>malloc(trialcomm.num_samples * sizeof(uint8_t*))
610
+ for comm_ix in range(trialcomm.num_samples):
611
+ trialcomm.comments[comm_ix] = <uint8_t *>malloc(256 * sizeof(uint8_t))
612
+ trial_comment.append([my_timestamps[comm_ix], trialcomm.comments[comm_ix],
613
+ my_charsets[comm_ix], my_rgbas[comm_ix]])
614
+
615
+ # cbsdk get trial data
616
+ # Note: continuous data was already fetched per-group above
617
+ try:
618
+ if do_event or do_comment:
619
+ res = cbsdk_get_trial_data(<uint32_t>instance, <int>seek,
620
+ &trialevent if do_event else NULL,
621
+ NULL, # continuous handled per-group above
622
+ &trialcomm if do_comment else NULL)
623
+ handle_result(res)
624
+ finally:
625
+ if do_comment:
626
+ free(trialcomm.comments)
627
+
628
+ # Note: tzero is no longer meaningful with group-based continuous data
629
+ return <int>res, trial_event, trial_cont, tzero, trial_comment
630
+
631
+
632
+ def trial_comment(int instance=0, bool seek=False, bool clock_reset=False, unsigned long wait_for_comment_msec=250):
633
+ """ Trial comment data.
634
+ Inputs:
635
+ seek - (optional) boolean
636
+ set False (default) to peek only and leave buffer intact.
637
+ set True to clear all the data and advance the data pointer.
638
+ clock_reset - (optional) boolean
639
+ set False (default) to leave the trial clock alone.
640
+ set True to update the _next_ trial time to the current time.
641
+ instance - (optional) library instance number
642
+ Outputs:
643
+ list of lists the form [timestamp, comment_str, rgba]
644
+ timestamp: PROCTIME
645
+ comment_str: the comment as a py string
646
+ rgba: integer; the comment colour. 8 bits each for r, g, b, a
647
+ """
648
+
649
+ cdef cbSdkResult res
650
+ cdef cbSdkTrialComment trialcomm
651
+
652
+ # get how many comments are available
653
+ res = cbsdk_init_trial_comment(<uint32_t>instance, <int>clock_reset, &trialcomm, wait_for_comment_msec)
654
+ handle_result(res)
655
+
656
+ if trialcomm.num_samples == 0:
657
+ return <int>res, []
658
+
659
+ # allocate memory
660
+
661
+ # types
662
+ cdef cnp.uint8_t[:] mxa_u8_cs # charsets
663
+ cdef cnp.uint32_t[:] mxa_u32_rgbas
664
+ cdef cnp.ndarray mxa_proctime
665
+
666
+ # For charsets;
667
+ mxa_u8_cs = np.zeros(trialcomm.num_samples, dtype=np.uint8)
668
+ trialcomm.charsets = <uint8_t *>&mxa_u8_cs[0]
669
+ my_charsets = np.asarray(mxa_u8_cs)
670
+
671
+ # For rgbas
672
+ mxa_u32_rgbas = np.zeros(trialcomm.num_samples, dtype=np.uint32)
673
+ trialcomm.rgbas = <uint32_t *>&mxa_u32_rgbas[0]
674
+ my_rgbas = np.asarray(mxa_u32_rgbas)
675
+
676
+ # For comments
677
+ trialcomm.comments = <uint8_t **>malloc(trialcomm.num_samples * sizeof(uint8_t*))
678
+ cdef int comm_ix
679
+ for comm_ix in range(trialcomm.num_samples):
680
+ trialcomm.comments[comm_ix] = <uint8_t *>malloc(256 * sizeof(uint8_t))
681
+
682
+ # For timestamps
683
+ mxa_proctime = np.zeros(trialcomm.num_samples, dtype=_PROCTIME_DTYPE)
684
+ trialcomm.timestamps = <PROCTIME *>cnp.PyArray_DATA(mxa_proctime)
685
+ my_timestamps = mxa_proctime
686
+
687
+ trial = []
688
+ try:
689
+ res = cbsdk_get_trial_comment(<int>instance, <int>seek, &trialcomm)
690
+ handle_result(res)
691
+ for comm_ix in range(trialcomm.num_samples):
692
+ # this_enc = 'utf-16' if my_charsets[comm_ix]==1 else locale.getpreferredencoding()
693
+ row = [my_timestamps[comm_ix], trialcomm.comments[comm_ix], my_rgbas[comm_ix]] # .decode(this_enc)
694
+ trial.append(row)
695
+ finally:
696
+ free(trialcomm.comments)
697
+
698
+ return <int>res, trial
699
+
700
+
701
+ def file_config(int instance=0, command='info', comment='', filename='', patient_info=None):
702
+ """ Configure remote file recording or get status of recording.
703
+ Inputs:
704
+ command - string, File configuration command, can be of of the following
705
+ 'info': (default) get File recording information
706
+ 'open': opens the File dialog if closed, ignoring other parameters
707
+ 'close': closes the File dialog if open
708
+ 'start': starts recording, opens dialog if closed
709
+ 'stop': stops recording
710
+ filename - (optional) string, file name to use for recording
711
+ comment - (optional) string, file comment to use for file recording
712
+ instance - (optional) library instance number
713
+ patient_info - (optional) dict carrying patient info. If provided then valid keys and value types:
714
+ 'ID': str - required, 'firstname': str - defaults to 'J', 'lastname': str - defaults to 'Doe',
715
+ 'DOBMonth': int - defaults to 01, 'DOBDay': int - defaults to 01, 'DOBYear': int - defaults to 1970
716
+ Outputs:
717
+ Only if command is 'info' output is returned
718
+ A dictionary with following keys:
719
+ 'Recording': boolean, if recording is in progress
720
+ 'FileName': string, file name being recorded
721
+ 'UserName': Computer that is recording
722
+ """
723
+
724
+
725
+ cdef cbSdkResult res
726
+ cdef char fname[256]
727
+ cdef char username[256]
728
+ cdef bool bRecording = False
729
+
730
+ if command == 'info':
731
+
732
+ res = cbSdkGetFileConfig(<uint32_t>instance, fname, username, &bRecording)
733
+ handle_result(res)
734
+ info = {'Recording':bRecording, 'FileName':<bytes>fname, 'UserName':<bytes>username}
735
+ return res, info
736
+
737
+ cdef int start = 0
738
+ cdef unsigned int options = cbFILECFG_OPT_NONE
739
+ if command == 'open':
740
+ if filename or comment:
741
+ raise RuntimeError('filename and comment must not be specified for open')
742
+ options = cbFILECFG_OPT_OPEN
743
+ elif command == 'close':
744
+ options = cbFILECFG_OPT_CLOSE
745
+ elif command == 'start':
746
+ if not filename:
747
+ raise RuntimeError('filename must be specified for start')
748
+ start = 1
749
+ elif command == 'stop':
750
+ if not filename:
751
+ raise RuntimeError('filename must be specified for stop')
752
+ start = 0
753
+ else:
754
+ raise RuntimeError("invalid file config command %s" % command)
755
+
756
+ cdef cbSdkResult patient_res
757
+ if start and patient_info is not None and 'ID' in patient_info:
758
+ default_patient_info = {'firstname': 'J', 'lastname': 'Doe', 'DOBMonth': 1, 'DOBDay': 1, 'DOBYear': 1970}
759
+ patient_info = {**default_patient_info, **patient_info}
760
+ for k,v in patient_info.items():
761
+ if type(v) is str:
762
+ patient_info[k] = v.encode('UTF-8')
763
+
764
+ patient_res = cbSdkSetPatientInfo(<uint32_t>instance,
765
+ <const char *>patient_info['ID'],
766
+ <const char *>patient_info['firstname'],
767
+ <const char *>patient_info['lastname'],
768
+ <uint32_t>patient_info['DOBMonth'],
769
+ <uint32_t>patient_info['DOBDay'],
770
+ <uint32_t>patient_info['DOBYear'])
771
+ handle_result(patient_res)
772
+
773
+ cdef int set_res
774
+ filename_string = filename.encode('UTF-8')
775
+ comment_string = comment.encode('UTF-8')
776
+ set_res = cbsdk_file_config(<uint32_t>instance, <const char *>filename_string, <char *>comment_string, start, options)
777
+
778
+
779
+ return set_res
780
+
781
+
782
+ def time(int instance=0, unit='samples'):
783
+ """Instrument time.
784
+ Inputs:
785
+ unit - time unit, string can be one the following
786
+ 'samples': (default) sample number integer
787
+ 'seconds' or 's': seconds calculated from samples
788
+ 'milliseconds' or 'ms': milliseconds calculated from samples
789
+ instance - (optional) library instance number
790
+ Outputs:
791
+ time - time passed since last reset
792
+ """
793
+
794
+
795
+ cdef cbSdkResult res
796
+
797
+ if unit == 'samples':
798
+ factor = 1
799
+ elif unit in ['seconds', 's']:
800
+ raise NotImplementedError("Use time unit of samples for now")
801
+ elif unit in ['milliseconds', 'ms']:
802
+ raise NotImplementedError("Use time unit of samples for now")
803
+ else:
804
+ raise RuntimeError("Invalid time unit %s" % unit)
805
+
806
+ cdef PROCTIME cbtime
807
+ res = cbSdkGetTime(<uint32_t>instance, &cbtime)
808
+ handle_result(res)
809
+
810
+ time = float(cbtime) / factor
811
+
812
+ return <int>res, time
813
+
814
+
815
+ def analog_out(channel_out, channel_mon, track_last=True, spike_only=False, int instance=0):
816
+ """
817
+ Monitor a channel.
818
+ Inputs:
819
+ channel_out - integer, analog output channel number (1-based)
820
+ On NSP, should be >= MIN_CHANS_ANALOG_OUT (273) && <= MAX_CHANS_AUDIO (278)
821
+ channel_mon - integer, channel to monitor (1-based)
822
+ track_last - (optional) If True, track last channel clicked on in raster plot or hardware config window.
823
+ spike_only - (optional) If True, only play spikes. If False, play continuous.
824
+ """
825
+ cdef cbSdkResult res
826
+ cdef cbSdkAoutMon mon
827
+ if channel_out < 273:
828
+ channel_out += 128 # Recent NSP firmware upgrade added 128 more analog channels.
829
+ if channel_mon is None:
830
+ res = cbSdkSetAnalogOutput(<uint32_t>instance, <uint16_t>channel_out, NULL, NULL)
831
+ else:
832
+ mon.chan = <uint16_t>channel_mon
833
+ mon.bTrack = track_last
834
+ mon.bSpike = spike_only
835
+ res = cbSdkSetAnalogOutput(<uint32_t>instance, <uint16_t>channel_out, NULL, &mon)
836
+ handle_result(res)
837
+ return <int>res
838
+
839
+
840
+ def digital_out(int channel, int instance=0, value='low'):
841
+ """Digital output command.
842
+ Inputs:
843
+ channel - integer, digital output channel number (1-based)
844
+ On NSP, 153 (dout1), 154 (dout2), 155 (dout3), 156 (dout4)
845
+ value - (optional), depends on the command
846
+ for command of 'set_value':
847
+ string, can be 'high' or 'low' (default)
848
+ instance - (optional) library instance number
849
+ """
850
+
851
+ values = ['low', 'high']
852
+ if value not in values:
853
+ raise RuntimeError("Invalid value %s" % value)
854
+
855
+ cdef cbSdkResult res
856
+ cdef uint16_t int_val = values.index(value)
857
+ res = cbSdkSetDigitalOutput(<uint32_t>instance, <uint16_t>channel, int_val)
858
+ handle_result(res)
859
+ return <int>res
860
+
861
+
862
+ def get_channel_config(int channel, int instance=0, encoding="utf-8") -> dict[str, typing.Any]:
863
+ """ Get channel configuration.
864
+ Inputs:
865
+ - channel: 1-based channel number
866
+ - instance: (optional) library instance number
867
+ - encoding: (optional) encoding for string fields, default = 'utf-8'
868
+ Outputs:
869
+ -chaninfo = A Python dictionary with the following fields:
870
+ 'time': system clock timestamp,
871
+ 'chid': 0x8000,
872
+ 'type': cbPKTTYPE_AINP*,
873
+ 'dlen': cbPKT_DLENCHANINFO,
874
+ 'chan': actual channel id of the channel being configured,
875
+ 'proc': the address of the processor on which the channel resides,
876
+ 'bank': the address of the bank on which the channel resides,
877
+ 'term': the terminal number of the channel within it's bank,
878
+ 'chancaps': general channel capablities (given by cbCHAN_* flags),
879
+ 'doutcaps': digital output capablities (composed of cbDOUT_* flags),
880
+ 'dinpcaps': digital input capablities (composed of cbDINP_* flags),
881
+ 'aoutcaps': analog output capablities (composed of cbAOUT_* flags),
882
+ 'ainpcaps': analog input capablities (composed of cbAINP_* flags),
883
+ 'spkcaps': spike processing capabilities,
884
+ 'label': Label of the channel (null terminated if <16 characters),
885
+ 'userflags': User flags for the channel state,
886
+ 'doutopts': digital output options (composed of cbDOUT_* flags),
887
+ 'dinpopts': digital input options (composed of cbDINP_* flags),
888
+ 'aoutopts': analog output options,
889
+ 'eopchar': digital input capablities (given by cbDINP_* flags),
890
+ 'ainpopts': analog input options (composed of cbAINP* flags),
891
+ 'smpfilter': continuous-time pathway filter id,
892
+ 'smpgroup': continuous-time pathway sample group,
893
+ 'smpdispmin': continuous-time pathway display factor,
894
+ 'smpdispmax': continuous-time pathway display factor,
895
+ 'trigtype': trigger type (see cbDOUT_TRIGGER_*),
896
+ 'trigchan': trigger channel,
897
+ 'trigval': trigger value,
898
+ 'lncrate': line noise cancellation filter adaptation rate,
899
+ 'spkfilter': spike pathway filter id,
900
+ 'spkdispmax': spike pathway display factor,
901
+ 'lncdispmax': Line Noise pathway display factor,
902
+ 'spkopts': spike processing options,
903
+ 'spkthrlevel': spike threshold level,
904
+ 'spkthrlimit': ,
905
+ 'spkgroup': NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping,
906
+ 'amplrejpos': Amplitude rejection positive value,
907
+ 'amplrejneg': Amplitude rejection negative value,
908
+ 'refelecchan': Software reference electrode channel,
909
+ """
910
+ cdef cbSdkResult res
911
+ cdef cbPKT_CHANINFO cb_chaninfo
912
+ res = cbSdkGetChannelConfig(<uint32_t>instance, <uint16_t>channel, &cb_chaninfo)
913
+ handle_result(res)
914
+ if res != 0:
915
+ return {}
916
+
917
+ chaninfo = {
918
+ 'time': cb_chaninfo.cbpkt_header.time,
919
+ 'chid': cb_chaninfo.cbpkt_header.chid,
920
+ 'type': cb_chaninfo.cbpkt_header.type, # cbPKTTYPE_AINP*
921
+ 'dlen': cb_chaninfo.cbpkt_header.dlen, # cbPKT_DLENCHANINFO
922
+ 'chan': cb_chaninfo.chan,
923
+ 'proc': cb_chaninfo.proc,
924
+ 'bank': cb_chaninfo.bank,
925
+ 'term': cb_chaninfo.term,
926
+ 'chancaps': cb_chaninfo.chancaps,
927
+ 'doutcaps': cb_chaninfo.doutcaps,
928
+ 'dinpcaps': cb_chaninfo.dinpcaps,
929
+ 'aoutcaps': cb_chaninfo.aoutcaps,
930
+ 'ainpcaps': cb_chaninfo.ainpcaps,
931
+ 'spkcaps': cb_chaninfo.spkcaps,
932
+ 'label': cb_chaninfo.label.decode(encoding),
933
+ 'userflags': cb_chaninfo.userflags,
934
+ 'doutopts': cb_chaninfo.doutopts,
935
+ 'dinpopts': cb_chaninfo.dinpopts,
936
+ 'moninst': cb_chaninfo.moninst,
937
+ 'monchan': cb_chaninfo.monchan,
938
+ 'outvalue': cb_chaninfo.outvalue,
939
+ 'aoutopts': cb_chaninfo.aoutopts,
940
+ 'eopchar': cb_chaninfo.eopchar,
941
+ 'ainpopts': cb_chaninfo.ainpopts,
942
+ 'smpfilter': cb_chaninfo.smpfilter,
943
+ 'smpgroup': cb_chaninfo.smpgroup,
944
+ 'smpdispmin': cb_chaninfo.smpdispmin,
945
+ 'smpdispmax': cb_chaninfo.smpdispmax,
946
+ 'trigtype': cb_chaninfo.trigtype,
947
+ 'trigchan': cb_chaninfo.trigchan,
948
+ 'lncrate': cb_chaninfo.lncrate,
949
+ 'spkfilter': cb_chaninfo.spkfilter,
950
+ 'spkdispmax': cb_chaninfo.spkdispmax,
951
+ 'lncdispmax': cb_chaninfo.lncdispmax,
952
+ 'spkopts': cb_chaninfo.spkopts,
953
+ 'spkthrlevel': cb_chaninfo.spkthrlevel,
954
+ 'spkthrlimit': cb_chaninfo.spkthrlimit,
955
+ 'spkgroup': cb_chaninfo.spkgroup,
956
+ 'amplrejpos': cb_chaninfo.amplrejpos,
957
+ 'amplrejneg': cb_chaninfo.amplrejneg,
958
+ 'refelecchan': cb_chaninfo.refelecchan
959
+ }
960
+
961
+ # TODO:
962
+ #cbSCALING physcalin # physical channel scaling information
963
+ #cbFILTDESC phyfiltin # physical channel filter definition
964
+ #cbSCALING physcalout # physical channel scaling information
965
+ #cbFILTDESC phyfiltout # physical channel filter definition
966
+ #int32_t position[4] # reserved for future position information
967
+ #cbSCALING scalin # user-defined scaling information for AINP
968
+ #cbSCALING scalout # user-defined scaling information for AOUT
969
+ #uint16_t moninst
970
+ #uint16_t monchan
971
+ #int32_t outvalue # output value
972
+ #uint16_t lowsamples # address of channel to monitor
973
+ #uint16_t highsamples # address of channel to monitor
974
+ #int32_t offset
975
+ #cbMANUALUNITMAPPING unitmapping[cbMAXUNITS+0] # manual unit mapping
976
+ #cbHOOP spkhoops[cbMAXUNITS+0][cbMAXHOOPS+0] # spike hoop sorting set
977
+
978
+ return chaninfo
979
+
980
+
981
+ def set_channel_config(int channel, chaninfo: dict[str, typing.Any] | None = None, int instance=0, encoding="utf-8") -> None:
982
+ """ Set channel configuration.
983
+ Inputs:
984
+ - channel: 1-based channel number
985
+ - chaninfo: A Python dict. See fields descriptions in get_channel_config. All fields are optional.
986
+ - instance: (optional) library instance number
987
+ - encoding: (optional) encoding for string fields, default = 'utf-8'
988
+ """
989
+ cdef cbSdkResult res
990
+ cdef cbPKT_CHANINFO cb_chaninfo
991
+ res = cbSdkGetChannelConfig(<uint32_t>instance, <uint16_t>channel, &cb_chaninfo)
992
+ handle_result(res)
993
+
994
+ if "label" in chaninfo:
995
+ new_label = chaninfo["label"]
996
+ if not isinstance(new_label, bytes):
997
+ new_label = new_label.encode(encoding)
998
+ strncpy(cb_chaninfo.label, new_label, sizeof(new_label))
999
+
1000
+ if "smpgroup" in chaninfo:
1001
+ cb_chaninfo.smpgroup = chaninfo["smpgroup"]
1002
+
1003
+ if "spkthrlevel" in chaninfo:
1004
+ cb_chaninfo.spkthrlevel = chaninfo["spkthrlevel"]
1005
+
1006
+ if "ainpopts" in chaninfo:
1007
+ cb_chaninfo.ainpopts = chaninfo["ainpopts"]
1008
+
1009
+ res = cbSdkSetChannelConfig(<uint32_t>instance, <uint16_t>channel, &cb_chaninfo)
1010
+ handle_result(res)
1011
+
1012
+
1013
+ def get_sample_group(int group_ix, int instance=0, encoding="utf-8", int proc=1) -> list[dict[str, typing.Any]]:
1014
+ """
1015
+ Get the channel infos for all channels in a sample group.
1016
+ Inputs:
1017
+ - group_ix: sample group index (1-6)
1018
+ - instance: (optional) library instance number
1019
+ - encoding: (optional) encoding for string fields, default = 'utf-8'
1020
+ Outputs:
1021
+ - channels_info: A list of dictionaries, one per channel in the group. Each dictionary has the following fields:
1022
+ 'chid': 0x8000,
1023
+ 'chan': actual channel id of the channel being configured,
1024
+ 'proc': the address of the processor on which the channel resides,
1025
+ 'bank': the address of the bank on which the channel resides,
1026
+ 'term': the terminal number of the channel within its bank,
1027
+ 'gain': the analog input gain (calculated from physical scaling),
1028
+ 'label': Label of the channel (null terminated if <16 characters),
1029
+ 'unit': physical unit of the channel,
1030
+ """
1031
+ cdef cbSdkResult res
1032
+ cdef uint32_t nChansInGroup
1033
+ cdef uint16_t pGroupList[cbNUM_ANALOG_CHANS+0]
1034
+
1035
+ res = cbSdkGetSampleGroupList(<uint32_t>instance, <uint32_t>proc, <uint32_t>group_ix, &nChansInGroup, pGroupList)
1036
+ if res:
1037
+ handle_result(res)
1038
+ return []
1039
+
1040
+ cdef cbPKT_CHANINFO chanInfo
1041
+ channels_info = []
1042
+ for chan_ix in range(nChansInGroup):
1043
+ chan_info = {}
1044
+ res = cbSdkGetChannelConfig(<uint32_t>instance, pGroupList[chan_ix], &chanInfo)
1045
+ handle_result(<cbSdkResult>res)
1046
+ ana_range = chanInfo.physcalin.anamax - chanInfo.physcalin.anamin
1047
+ dig_range = chanInfo.physcalin.digmax - chanInfo.physcalin.digmin
1048
+ chan_info["chan"] = chanInfo.chan
1049
+ chan_info["proc"] = chanInfo.proc
1050
+ chan_info["bank"] = chanInfo.bank
1051
+ chan_info["term"] = chanInfo.term
1052
+ chan_info["gain"] = ana_range / dig_range
1053
+ chan_info["label"] = chanInfo.label.decode(encoding)
1054
+ chan_info["unit"] = chanInfo.physcalin.anaunit.decode(encoding)
1055
+ channels_info.append(chan_info)
1056
+
1057
+ return channels_info
1058
+
1059
+
1060
+ def set_comment(comment_string, rgba_tuple=(0, 0, 0, 255), int instance=0):
1061
+ """rgba_tuple is actually t_bgr: transparency, blue, green, red. Default: no-transparency & red."""
1062
+ cdef cbSdkResult res
1063
+ cdef uint32_t t_bgr = (rgba_tuple[0] << 24) + (rgba_tuple[1] << 16) + (rgba_tuple[2] << 8) + rgba_tuple[3]
1064
+ cdef uint8_t charset = 0 # Character set (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI)
1065
+ cdef bytes py_bytes = comment_string.encode()
1066
+ cdef const char* comment = py_bytes
1067
+ res = cbSdkSetComment(<uint32_t> instance, t_bgr, charset, comment)
1068
+
1069
+
1070
+ def get_sys_config(int instance=0):
1071
+ cdef cbSdkResult res
1072
+ cdef uint32_t spklength
1073
+ cdef uint32_t spkpretrig
1074
+ cdef uint32_t sysfreq
1075
+ res = cbSdkGetSysConfig(<uint32_t> instance, &spklength, &spkpretrig, &sysfreq)
1076
+ handle_result(res)
1077
+ return {'spklength': spklength, 'spkpretrig': spkpretrig, 'sysfreq': sysfreq}
1078
+
1079
+
1080
+ def set_spike_config(int spklength=48, int spkpretrig=10, int instance=0):
1081
+ cdef cbSdkResult res
1082
+ res = cbSdkSetSpikeConfig(<uint32_t> instance, <uint32_t> spklength, <uint32_t> spkpretrig)
1083
+ handle_result(res)
1084
+
1085
+
1086
+ cdef extern from "numpy/arrayobject.h":
1087
+ void PyArray_ENABLEFLAGS(cnp.ndarray arr, int flags)
1088
+
1089
+
1090
+ cdef class SpikeCache:
1091
+ cdef readonly int inst, chan, n_samples, n_pretrig
1092
+ cdef cbSPKCACHE *p_cache
1093
+ cdef int last_valid
1094
+ # cache
1095
+ # .chid: ID of the Channel
1096
+ # .pktcnt: number of packets that can be saved
1097
+ # .pktsize: size of an individual packet
1098
+ # .head: Where (0 based index) in the circular buffer to place the NEXT packet
1099
+ # .valid: How many packets have come in since the last configuration
1100
+ # .spkpkt: Circular buffer of the cached spikes
1101
+
1102
+ def __cinit__(self, int channel=1, int instance=0):
1103
+ self.inst = instance
1104
+ self.chan = channel
1105
+ cdef cbSPKCACHE ignoreme # Just so self.p_cache is not NULL... but this won't be used by anything
1106
+ self.p_cache = &ignoreme # because cbSdkGetSpkCache changes what self.p_cache is pointing to.
1107
+ self.reset_cache()
1108
+ sys_config_dict = get_sys_config(instance)
1109
+ self.n_samples = sys_config_dict['spklength']
1110
+ self.n_pretrig = sys_config_dict['spkpretrig']
1111
+
1112
+ def reset_cache(self):
1113
+ cdef cbSdkResult res = cbSdkGetSpkCache(self.inst, self.chan, &self.p_cache)
1114
+ handle_result(res)
1115
+ self.last_valid = self.p_cache.valid
1116
+
1117
+ # This function needs to be FAST!
1118
+ @cython.boundscheck(False) # turn off bounds-checking for entire function
1119
+ def get_new_waveforms(self):
1120
+ # cdef everything!
1121
+ cdef int new_valid = self.p_cache.valid
1122
+ cdef int new_head = self.p_cache.head
1123
+ cdef int n_new = min(max(new_valid - self.last_valid, 0), 400)
1124
+ cdef cnp.ndarray[cnp.int16_t, ndim=2, mode="c"] np_waveforms = np.empty((n_new, self.n_samples), dtype=np.int16)
1125
+ cdef cnp.ndarray[cnp.uint16_t, ndim=1] np_unit_ids = np.empty(n_new, dtype=np.uint16)
1126
+ cdef int wf_ix, pkt_ix, samp_ix
1127
+ for wf_ix in range(n_new):
1128
+ pkt_ix = (new_head - 2 - n_new + wf_ix) % 400
1129
+ np_unit_ids[wf_ix] = self.p_cache.spkpkt[pkt_ix].cbpkt_header.type
1130
+ # Instead of per-sample copy, we could copy the pointer for the whole wave to the buffer of a 1-d np array,
1131
+ # then use memory view copying from 1-d array into our 2d matrix. But below is pure-C so should be fast too.
1132
+ for samp_ix in range(self.n_samples):
1133
+ np_waveforms[wf_ix, samp_ix] = self.p_cache.spkpkt[pkt_ix].wave[samp_ix]
1134
+ #unit_ids_out = [<int>unit_ids[wf_ix] for wf_ix in range(n_new)]
1135
+ PyArray_ENABLEFLAGS(np_waveforms, NPY_ARRAY_OWNDATA)
1136
+ self.last_valid = new_valid
1137
+ return np_waveforms, np_unit_ids
1138
+
1139
+
1140
+ cdef cbSdkResult handle_result(cbSdkResult res):
1141
+ if res == CBSDKRESULT_WARNCLOSED:
1142
+ print("Library is already closed.")
1143
+ if res < 0:
1144
+ errtext = "No associated error string. See cbsdk.h"
1145
+ if res == CBSDKRESULT_ERROFFLINE:
1146
+ errtext = "Instrument is offline."
1147
+ elif res == CBSDKRESULT_CLOSED:
1148
+ errtext = "Interface is closed; cannot do this operation."
1149
+ elif res == CBSDKRESULT_ERRCONFIG:
1150
+ errtext = "Trying to run an unconfigured method."
1151
+ elif res == CBSDKRESULT_NULLPTR:
1152
+ errtext = "Null pointer."
1153
+ elif res == CBSDKRESULT_INVALIDCHANNEL:
1154
+ errtext = "Invalid channel number."
1155
+
1156
+ raise RuntimeError(("%d, " + errtext) % res)
1157
+ return res