Spaces:
Sleeping
Sleeping
ybouteiller
commited on
Commit
·
c9c81f5
1
Parent(s):
15952ff
new interface, lsl, custom filtering
Browse files- portiloop/capture.py +255 -46
- portiloop/notebooks/test_LSL.ipynb +136 -0
- portiloop/notebooks/tests.ipynb +25 -2
- setup.py +3 -1
portiloop/capture.py
CHANGED
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
1 |
from time import sleep
|
2 |
import time
|
3 |
import numpy as np
|
@@ -12,6 +15,7 @@ from threading import Thread, Lock
|
|
12 |
|
13 |
import matplotlib.pyplot as plt
|
14 |
from EDFlib.edfwriter import EDFwriter
|
|
|
15 |
|
16 |
from portilooplot.jupyter_plot import ProgressPlot
|
17 |
from portiloop.hardware.frontend import Frontend
|
@@ -82,6 +86,17 @@ FRONTEND_CONFIG = [
|
|
82 |
|
83 |
EDF_PATH = Path.home() / 'workspace' / 'edf_recording'
|
84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
def mod_config(config, datarate, channel_modes):
|
86 |
|
87 |
# datarate:
|
@@ -100,7 +115,7 @@ def mod_config(config, datarate, channel_modes):
|
|
100 |
break
|
101 |
|
102 |
new_cf1 = config[1] & 0xF8
|
103 |
-
new_cf1 = new_cf1 |
|
104 |
config[1] = new_cf1
|
105 |
|
106 |
# bias:
|
@@ -178,9 +193,18 @@ class FIR:
|
|
178 |
|
179 |
|
180 |
class FilterPipeline:
|
181 |
-
def __init__(self,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
self.nb_channels = nb_channels
|
183 |
-
assert power_line_fq in [50, 60], f"The only supported power line frequencies are
|
184 |
if power_line_fq == 60:
|
185 |
self.notch_coeff1 = -0.12478308884588535
|
186 |
self.notch_coeff2 = 0.98729186796473023
|
@@ -197,33 +221,36 @@ class FilterPipeline:
|
|
197 |
|
198 |
self.moving_average = None
|
199 |
self.moving_variance = np.zeros(self.nb_channels)
|
200 |
-
self.ALPHA_AVG =
|
201 |
-
self.ALPHA_STD =
|
202 |
-
self.EPSILON =
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
|
|
|
|
|
|
227 |
|
228 |
def filter(self, value):
|
229 |
"""
|
@@ -370,7 +397,7 @@ def _capture_process(p_data_o, p_msg_io, duration, frequency, python_clock, time
|
|
370 |
p_msg_io.close()
|
371 |
p_data_o.close()
|
372 |
|
373 |
-
|
374 |
class Capture:
|
375 |
def __init__(self):
|
376 |
# {now.strftime('%m_%d_%Y_%H_%M_%S')}
|
@@ -379,8 +406,16 @@ class Capture:
|
|
379 |
self.__capture_on = False
|
380 |
self.frequency = 250
|
381 |
self.duration = 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
self.filter = True
|
383 |
self.record = False
|
|
|
384 |
self.display = False
|
385 |
self.python_clock = True
|
386 |
self.edf_writer = None
|
@@ -478,7 +513,6 @@ class Capture:
|
|
478 |
disabled=False,
|
479 |
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
480 |
tooltips=['Stop capture', 'Start capture'],
|
481 |
-
# icons=['check'] * 2
|
482 |
)
|
483 |
|
484 |
self.b_clock = widgets.ToggleButtons(
|
@@ -488,7 +522,24 @@ class Capture:
|
|
488 |
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
489 |
tooltips=['Use Coral clock (very precise, not very timely)',
|
490 |
'Use ADS clock (not very precise, very timely)'],
|
491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
492 |
)
|
493 |
|
494 |
self.b_filename = widgets.Text(
|
@@ -498,33 +549,83 @@ class Capture:
|
|
498 |
)
|
499 |
|
500 |
self.b_frequency = widgets.IntText(
|
501 |
-
value=
|
502 |
description='Freq (Hz):',
|
503 |
disabled=False
|
504 |
)
|
505 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
506 |
self.b_duration = widgets.IntText(
|
507 |
-
value=
|
508 |
description='Time (s):',
|
509 |
disabled=False
|
510 |
)
|
511 |
|
512 |
self.b_filter = widgets.Checkbox(
|
513 |
-
value=
|
514 |
description='Filter',
|
515 |
disabled=False,
|
516 |
indent=False
|
517 |
)
|
518 |
|
519 |
self.b_record = widgets.Checkbox(
|
520 |
-
value=
|
521 |
-
description='Record',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
522 |
disabled=False,
|
523 |
indent=False
|
524 |
)
|
525 |
|
526 |
self.b_display = widgets.Checkbox(
|
527 |
-
value=
|
528 |
description='Display',
|
529 |
disabled=False,
|
530 |
indent=False
|
@@ -538,6 +639,7 @@ class Capture:
|
|
538 |
self.b_duration.observe(self.on_b_duration, 'value')
|
539 |
self.b_filter.observe(self.on_b_filter, 'value')
|
540 |
self.b_record.observe(self.on_b_record, 'value')
|
|
|
541 |
self.b_display.observe(self.on_b_display, 'value')
|
542 |
self.b_filename.observe(self.on_b_filename, 'value')
|
543 |
self.b_radio_ch2.observe(self.on_b_radio_ch2, 'value')
|
@@ -546,6 +648,13 @@ class Capture:
|
|
546 |
self.b_radio_ch5.observe(self.on_b_radio_ch5, 'value')
|
547 |
self.b_radio_ch6.observe(self.on_b_radio_ch6, 'value')
|
548 |
self.b_radio_ch7.observe(self.on_b_radio_ch7, 'value')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
549 |
|
550 |
self.display_buttons()
|
551 |
|
@@ -557,8 +666,10 @@ class Capture:
|
|
557 |
self.b_frequency,
|
558 |
self.b_duration,
|
559 |
self.b_filename,
|
560 |
-
|
561 |
self.b_clock,
|
|
|
|
|
562 |
self.b_capture]))
|
563 |
|
564 |
def enable_buttons(self):
|
@@ -567,6 +678,7 @@ class Capture:
|
|
567 |
self.b_filename.disabled = False
|
568 |
self.b_filter.disabled = False
|
569 |
self.b_record.disabled = False
|
|
|
570 |
self.b_display.disabled = False
|
571 |
self.b_clock.disabled = False
|
572 |
self.b_radio_ch2.disabled = False
|
@@ -575,6 +687,13 @@ class Capture:
|
|
575 |
self.b_radio_ch5.disabled = False
|
576 |
self.b_radio_ch6.disabled = False
|
577 |
self.b_radio_ch7.disabled = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
578 |
|
579 |
def disable_buttons(self):
|
580 |
self.b_frequency.disabled = True
|
@@ -582,6 +701,7 @@ class Capture:
|
|
582 |
self.b_filename.disabled = True
|
583 |
self.b_filter.disabled = True
|
584 |
self.b_record.disabled = True
|
|
|
585 |
self.b_display.disabled = True
|
586 |
self.b_clock.disabled = True
|
587 |
self.b_radio_ch2.disabled = True
|
@@ -590,6 +710,13 @@ class Capture:
|
|
590 |
self.b_radio_ch5.disabled = True
|
591 |
self.b_radio_ch6.disabled = True
|
592 |
self.b_radio_ch7.disabled = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
593 |
|
594 |
def on_b_radio_ch2(self, value):
|
595 |
self.channel_states[1] = value['new']
|
@@ -614,6 +741,9 @@ class Capture:
|
|
614 |
if val == 'Start':
|
615 |
clear_output()
|
616 |
self.disable_buttons()
|
|
|
|
|
|
|
617 |
self.display_buttons()
|
618 |
with self._lock_msg_out:
|
619 |
self._msg_out = None
|
@@ -621,7 +751,7 @@ class Capture:
|
|
621 |
warnings.warn("Capture already running, operation aborted.")
|
622 |
return
|
623 |
self._t_capture = Thread(target=self.start_capture,
|
624 |
-
args=(self.filter, self.record, self.display, 500, self.python_clock))
|
625 |
self._t_capture.start()
|
626 |
elif val == 'Stop':
|
627 |
with self._lock_msg_out:
|
@@ -631,6 +761,14 @@ class Capture:
|
|
631 |
self._t_capture = None
|
632 |
self.enable_buttons()
|
633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
634 |
def on_b_clock(self, value):
|
635 |
val = value['new']
|
636 |
if val == 'Coral':
|
@@ -638,10 +776,19 @@ class Capture:
|
|
638 |
elif val == 'ADS':
|
639 |
self.python_clock = False
|
640 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
641 |
def on_b_frequency(self, value):
|
642 |
val = value['new']
|
643 |
if val > 0:
|
644 |
self.frequency = val
|
|
|
|
|
645 |
|
646 |
def on_b_filename(self, value):
|
647 |
val = value['new']
|
@@ -658,6 +805,41 @@ class Capture:
|
|
658 |
if val > 0:
|
659 |
self.duration = val
|
660 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
661 |
def on_b_filter(self, value):
|
662 |
val = value['new']
|
663 |
self.filter = val
|
@@ -666,6 +848,10 @@ class Capture:
|
|
666 |
val = value['new']
|
667 |
self.record = val
|
668 |
|
|
|
|
|
|
|
|
|
669 |
def on_b_display(self, value):
|
670 |
val = value['new']
|
671 |
self.display = val
|
@@ -709,6 +895,7 @@ class Capture:
|
|
709 |
def start_capture(self,
|
710 |
filter,
|
711 |
record,
|
|
|
712 |
viz,
|
713 |
width,
|
714 |
python_clock):
|
@@ -720,7 +907,18 @@ class Capture:
|
|
720 |
p_msg_io, p_msg_io_2 = mp.Pipe()
|
721 |
p_data_i, p_data_o = mp.Pipe(duplex=False)
|
722 |
SAMPLE_TIME = 1 / self.frequency
|
723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
724 |
self._p_capture = mp.Process(target=_capture_process,
|
725 |
args=(p_data_o,
|
726 |
p_msg_io_2,
|
@@ -738,6 +936,15 @@ class Capture:
|
|
738 |
|
739 |
if record:
|
740 |
self.open_recording_file()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
741 |
|
742 |
buffer = []
|
743 |
|
@@ -765,11 +972,13 @@ class Capture:
|
|
765 |
|
766 |
if filter:
|
767 |
n_array = fp.filter(n_array)
|
768 |
-
# n_array = np.swapaxes(n_array, 0, 1)
|
769 |
-
# n_array = np.array([fp_vec[i].filter(a) if self.channel_states[i] != 'disabled' else [0] for i, a in enumerate(n_array)])
|
770 |
-
# n_array = np.swapaxes(n_array, 0, 1)
|
771 |
|
772 |
-
|
|
|
|
|
|
|
|
|
|
|
773 |
if len(buffer) >= 50:
|
774 |
|
775 |
if viz:
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
|
4 |
from time import sleep
|
5 |
import time
|
6 |
import numpy as np
|
|
|
15 |
|
16 |
import matplotlib.pyplot as plt
|
17 |
from EDFlib.edfwriter import EDFwriter
|
18 |
+
from scipy.signal import firwin
|
19 |
|
20 |
from portilooplot.jupyter_plot import ProgressPlot
|
21 |
from portiloop.hardware.frontend import Frontend
|
|
|
86 |
|
87 |
EDF_PATH = Path.home() / 'workspace' / 'edf_recording'
|
88 |
|
89 |
+
|
90 |
+
def to_ads_frequency(frequency):
|
91 |
+
possible_datarates = [250, 500, 1000, 2000, 4000, 8000, 16000]
|
92 |
+
dr = 16000
|
93 |
+
for i in possible_datarates:
|
94 |
+
if i >= datarate:
|
95 |
+
dr = i
|
96 |
+
break
|
97 |
+
return dr
|
98 |
+
|
99 |
+
|
100 |
def mod_config(config, datarate, channel_modes):
|
101 |
|
102 |
# datarate:
|
|
|
115 |
break
|
116 |
|
117 |
new_cf1 = config[1] & 0xF8
|
118 |
+
new_cf1 = new_cf1 | mod_dr
|
119 |
config[1] = new_cf1
|
120 |
|
121 |
# bias:
|
|
|
193 |
|
194 |
|
195 |
class FilterPipeline:
|
196 |
+
def __init__(self,
|
197 |
+
nb_channels,
|
198 |
+
sampling_rate,
|
199 |
+
power_line_fq=60,
|
200 |
+
use_custom_fir=False,
|
201 |
+
custom_fir_order=10,
|
202 |
+
custom_fir_cutoff=30,
|
203 |
+
alpha_avg=0.1,
|
204 |
+
alpha_std=0.001,
|
205 |
+
epsilon=0.000001):
|
206 |
self.nb_channels = nb_channels
|
207 |
+
assert power_line_fq in [50, 60], f"The only supported power line frequencies are 50 Hz and 60 Hz"
|
208 |
if power_line_fq == 60:
|
209 |
self.notch_coeff1 = -0.12478308884588535
|
210 |
self.notch_coeff2 = 0.98729186796473023
|
|
|
221 |
|
222 |
self.moving_average = None
|
223 |
self.moving_variance = np.zeros(self.nb_channels)
|
224 |
+
self.ALPHA_AVG = alpha_avg
|
225 |
+
self.ALPHA_STD = alpha_std
|
226 |
+
self.EPSILON = epsilon
|
227 |
+
|
228 |
+
if use_custom_fir:
|
229 |
+
self.fir_coef = firwin(numtaps=custom_fir_order+1, cutoff=custom_fir_cutoff, fs=sampling_rate)
|
230 |
+
else:
|
231 |
+
self.fir_coef = [
|
232 |
+
0.001623780150148094927192721215192250384,
|
233 |
+
0.014988684599373741992978104065059596905,
|
234 |
+
0.021287595318265635502275046064823982306,
|
235 |
+
0.007349500393709578957568417933998716762,
|
236 |
+
-0.025127515717112181709014251396183681209,
|
237 |
+
-0.052210507359822452833064687638398027048,
|
238 |
+
-0.039273839505489904766477593511808663607,
|
239 |
+
0.033021568427940004020193498490698402748,
|
240 |
+
0.147606943281569008563636202779889572412,
|
241 |
+
0.254000252034505602516389899392379447818,
|
242 |
+
0.297330876398883392486283128164359368384,
|
243 |
+
0.254000252034505602516389899392379447818,
|
244 |
+
0.147606943281569008563636202779889572412,
|
245 |
+
0.033021568427940004020193498490698402748,
|
246 |
+
-0.039273839505489904766477593511808663607,
|
247 |
+
-0.052210507359822452833064687638398027048,
|
248 |
+
-0.025127515717112181709014251396183681209,
|
249 |
+
0.007349500393709578957568417933998716762,
|
250 |
+
0.021287595318265635502275046064823982306,
|
251 |
+
0.014988684599373741992978104065059596905,
|
252 |
+
0.001623780150148094927192721215192250384]
|
253 |
+
self.fir = FIR(self.nb_channels, self.fir_coef)
|
254 |
|
255 |
def filter(self, value):
|
256 |
"""
|
|
|
397 |
p_msg_io.close()
|
398 |
p_data_o.close()
|
399 |
|
400 |
+
|
401 |
class Capture:
|
402 |
def __init__(self):
|
403 |
# {now.strftime('%m_%d_%Y_%H_%M_%S')}
|
|
|
406 |
self.__capture_on = False
|
407 |
self.frequency = 250
|
408 |
self.duration = 10
|
409 |
+
self.power_line = 60
|
410 |
+
self.polyak_mean = 0.1
|
411 |
+
self.polyak_std = 0.001
|
412 |
+
self.epsilon = 0.000001
|
413 |
+
self.custom_fir = False
|
414 |
+
self.custom_fir_order = 10
|
415 |
+
self.custom_fir_cutoff = 30
|
416 |
self.filter = True
|
417 |
self.record = False
|
418 |
+
self.lsl = False
|
419 |
self.display = False
|
420 |
self.python_clock = True
|
421 |
self.edf_writer = None
|
|
|
513 |
disabled=False,
|
514 |
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
515 |
tooltips=['Stop capture', 'Start capture'],
|
|
|
516 |
)
|
517 |
|
518 |
self.b_clock = widgets.ToggleButtons(
|
|
|
522 |
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
523 |
tooltips=['Use Coral clock (very precise, not very timely)',
|
524 |
'Use ADS clock (not very precise, very timely)'],
|
525 |
+
)
|
526 |
+
|
527 |
+
self.b_power_line = widgets.ToggleButtons(
|
528 |
+
options=['60 Hz', '50 Hz'],
|
529 |
+
description='Power line:',
|
530 |
+
disabled=False,
|
531 |
+
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
532 |
+
tooltips=['North America 60 Hz',
|
533 |
+
'Europe 50 Hz'],
|
534 |
+
)
|
535 |
+
|
536 |
+
self.b_custom_fir = widgets.ToggleButtons(
|
537 |
+
options=['Default', 'Custom'],
|
538 |
+
description='FIR filter:',
|
539 |
+
disabled=False,
|
540 |
+
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
541 |
+
tooltips=['Use the default 30Hz low-pass FIR from the Portiloop paper',
|
542 |
+
'Use a custom FIR'],
|
543 |
)
|
544 |
|
545 |
self.b_filename = widgets.Text(
|
|
|
549 |
)
|
550 |
|
551 |
self.b_frequency = widgets.IntText(
|
552 |
+
value=self.frequency,
|
553 |
description='Freq (Hz):',
|
554 |
disabled=False
|
555 |
)
|
556 |
|
557 |
+
self.b_polyak_mean = widgets.FloatText(
|
558 |
+
value=self.polyak_mean,
|
559 |
+
description='Polyak mean:',
|
560 |
+
disabled=False
|
561 |
+
)
|
562 |
+
|
563 |
+
self.b_polyak_std = widgets.FloatText(
|
564 |
+
value=self.polyak_std,
|
565 |
+
description='Polyak std:',
|
566 |
+
disabled=False
|
567 |
+
)
|
568 |
+
|
569 |
+
self.b_epsilon = widgets.FloatText(
|
570 |
+
value=self.epsilon,
|
571 |
+
description='Epsilon:',
|
572 |
+
disabled=False
|
573 |
+
)
|
574 |
+
|
575 |
+
self.b_custom_fir_order = widgets.IntText(
|
576 |
+
value=self.custom_fir_order,
|
577 |
+
description='FIR order:',
|
578 |
+
disabled=True
|
579 |
+
)
|
580 |
+
|
581 |
+
self.b_custom_fir_cutoff = widgets.IntText(
|
582 |
+
value=self.custom_fir_cutoff,
|
583 |
+
description='FIR cutoff:',
|
584 |
+
disabled=True
|
585 |
+
)
|
586 |
+
|
587 |
+
self.b_accordion_filter = widgets.Accordion(
|
588 |
+
children=[
|
589 |
+
widgets.VBox([
|
590 |
+
self.b_custom_fir,
|
591 |
+
self.b_custom_fir_order,
|
592 |
+
self.b_custom_fir_cutoff,
|
593 |
+
self.b_polyak_mean,
|
594 |
+
self.b_polyak_std,
|
595 |
+
self.b_epsilon
|
596 |
+
])
|
597 |
+
])
|
598 |
+
self.b_accordion_filter.set_title(index = 0, title = 'Filtering')
|
599 |
+
|
600 |
self.b_duration = widgets.IntText(
|
601 |
+
value=self.duration,
|
602 |
description='Time (s):',
|
603 |
disabled=False
|
604 |
)
|
605 |
|
606 |
self.b_filter = widgets.Checkbox(
|
607 |
+
value=self.filter,
|
608 |
description='Filter',
|
609 |
disabled=False,
|
610 |
indent=False
|
611 |
)
|
612 |
|
613 |
self.b_record = widgets.Checkbox(
|
614 |
+
value=self.record,
|
615 |
+
description='Record EDF',
|
616 |
+
disabled=False,
|
617 |
+
indent=False
|
618 |
+
)
|
619 |
+
|
620 |
+
self.b_lsl = widgets.Checkbox(
|
621 |
+
value=self.lsl,
|
622 |
+
description='Stream LSL',
|
623 |
disabled=False,
|
624 |
indent=False
|
625 |
)
|
626 |
|
627 |
self.b_display = widgets.Checkbox(
|
628 |
+
value=self.display,
|
629 |
description='Display',
|
630 |
disabled=False,
|
631 |
indent=False
|
|
|
639 |
self.b_duration.observe(self.on_b_duration, 'value')
|
640 |
self.b_filter.observe(self.on_b_filter, 'value')
|
641 |
self.b_record.observe(self.on_b_record, 'value')
|
642 |
+
self.b_lsl.observe(self.on_b_lsl, 'value')
|
643 |
self.b_display.observe(self.on_b_display, 'value')
|
644 |
self.b_filename.observe(self.on_b_filename, 'value')
|
645 |
self.b_radio_ch2.observe(self.on_b_radio_ch2, 'value')
|
|
|
648 |
self.b_radio_ch5.observe(self.on_b_radio_ch5, 'value')
|
649 |
self.b_radio_ch6.observe(self.on_b_radio_ch6, 'value')
|
650 |
self.b_radio_ch7.observe(self.on_b_radio_ch7, 'value')
|
651 |
+
self.b_power_line.observe(self.on_b_power_line, 'value')
|
652 |
+
self.b_custom_fir.observe(self.on_b_custom_fir, 'value')
|
653 |
+
self.b_custom_fir_order.observe(self.on_b_custom_fir_order, 'value')
|
654 |
+
self.b_custom_fir_cutoff.observe(self.on_b_custom_fir_cutoff, 'value')
|
655 |
+
self.b_polyak_mean.observe(self.on_b_polyak_mean, 'value')
|
656 |
+
self.b_polyak_std.observe(self.on_b_polyak_std, 'value')
|
657 |
+
self.b_epsilon.observe(self.on_b_epsilon, 'value')
|
658 |
|
659 |
self.display_buttons()
|
660 |
|
|
|
666 |
self.b_frequency,
|
667 |
self.b_duration,
|
668 |
self.b_filename,
|
669 |
+
self.b_power_line,
|
670 |
self.b_clock,
|
671 |
+
widgets.HBox([self.b_filter, self.b_record, self.b_lsl, self.b_display]),
|
672 |
+
self.b_accordion_filter,
|
673 |
self.b_capture]))
|
674 |
|
675 |
def enable_buttons(self):
|
|
|
678 |
self.b_filename.disabled = False
|
679 |
self.b_filter.disabled = False
|
680 |
self.b_record.disabled = False
|
681 |
+
self.b_record.lsl = False
|
682 |
self.b_display.disabled = False
|
683 |
self.b_clock.disabled = False
|
684 |
self.b_radio_ch2.disabled = False
|
|
|
687 |
self.b_radio_ch5.disabled = False
|
688 |
self.b_radio_ch6.disabled = False
|
689 |
self.b_radio_ch7.disabled = False
|
690 |
+
self.b_power_line.disabled = False
|
691 |
+
self.b_polyak_mean.disabled = False
|
692 |
+
self.b_polyak_std.disabled = False
|
693 |
+
self.b_epsilon.disabled = False
|
694 |
+
self.b_custom_fir.disabled = False
|
695 |
+
self.b_custom_fir_order.disabled = not self.custom_fir
|
696 |
+
self.b_custom_fir_cutoff.disabled = not self.custom_fir
|
697 |
|
698 |
def disable_buttons(self):
|
699 |
self.b_frequency.disabled = True
|
|
|
701 |
self.b_filename.disabled = True
|
702 |
self.b_filter.disabled = True
|
703 |
self.b_record.disabled = True
|
704 |
+
self.b_record.lsl = True
|
705 |
self.b_display.disabled = True
|
706 |
self.b_clock.disabled = True
|
707 |
self.b_radio_ch2.disabled = True
|
|
|
710 |
self.b_radio_ch5.disabled = True
|
711 |
self.b_radio_ch6.disabled = True
|
712 |
self.b_radio_ch7.disabled = True
|
713 |
+
self.b_power_line.disabled = True
|
714 |
+
self.b_polyak_mean.disabled = True
|
715 |
+
self.b_polyak_std.disabled = True
|
716 |
+
self.b_epsilon.disabled = True
|
717 |
+
self.b_custom_fir.disabled = True
|
718 |
+
self.b_custom_fir_order.disabled = True
|
719 |
+
self.b_custom_fir_cutoff.disabled = True
|
720 |
|
721 |
def on_b_radio_ch2(self, value):
|
722 |
self.channel_states[1] = value['new']
|
|
|
741 |
if val == 'Start':
|
742 |
clear_output()
|
743 |
self.disable_buttons()
|
744 |
+
if not self.python_clock: # ADS clock: force the frequency to an ADS-compatible frequency
|
745 |
+
self.frequency = to_ads_frequency(self.frequency)
|
746 |
+
self.b_frequency.value = self.frequency
|
747 |
self.display_buttons()
|
748 |
with self._lock_msg_out:
|
749 |
self._msg_out = None
|
|
|
751 |
warnings.warn("Capture already running, operation aborted.")
|
752 |
return
|
753 |
self._t_capture = Thread(target=self.start_capture,
|
754 |
+
args=(self.filter, self.record, self.lsl, self.display, 500, self.python_clock))
|
755 |
self._t_capture.start()
|
756 |
elif val == 'Stop':
|
757 |
with self._lock_msg_out:
|
|
|
761 |
self._t_capture = None
|
762 |
self.enable_buttons()
|
763 |
|
764 |
+
def on_b_custom_fir(self, value):
|
765 |
+
val = value['new']
|
766 |
+
if val == 'Default':
|
767 |
+
self.custom_fir = False
|
768 |
+
elif val == 'Custom':
|
769 |
+
self.custom_fir = True
|
770 |
+
self.enable_buttons()
|
771 |
+
|
772 |
def on_b_clock(self, value):
|
773 |
val = value['new']
|
774 |
if val == 'Coral':
|
|
|
776 |
elif val == 'ADS':
|
777 |
self.python_clock = False
|
778 |
|
779 |
+
def on_b_power_line(self, value):
|
780 |
+
val = value['new']
|
781 |
+
if val == '60 Hz':
|
782 |
+
self.power_line = 60
|
783 |
+
elif val == '50 Hz':
|
784 |
+
self.python_clock = 50
|
785 |
+
|
786 |
def on_b_frequency(self, value):
|
787 |
val = value['new']
|
788 |
if val > 0:
|
789 |
self.frequency = val
|
790 |
+
else:
|
791 |
+
self.b_frequency.value = self.frequency
|
792 |
|
793 |
def on_b_filename(self, value):
|
794 |
val = value['new']
|
|
|
805 |
if val > 0:
|
806 |
self.duration = val
|
807 |
|
808 |
+
def on_b_custom_fir_order(self, value):
|
809 |
+
val = value['new']
|
810 |
+
if val > 0:
|
811 |
+
self.custom_fir_order = val
|
812 |
+
else:
|
813 |
+
self.b_custom_fir_order.value = self.custom_fir_order
|
814 |
+
|
815 |
+
def on_b_custom_fir_cutoff(self, value):
|
816 |
+
val = value['new']
|
817 |
+
if val > 0 and val < self.frequency / 2:
|
818 |
+
self.custom_fir_cutoff = val
|
819 |
+
else:
|
820 |
+
self.b_custom_fir_cutoff.value = self.custom_fir_cutoff
|
821 |
+
|
822 |
+
def on_b_polyak_mean(self, value):
|
823 |
+
val = value['new']
|
824 |
+
if val >= 0 and val <= 1:
|
825 |
+
self.polyak_mean = val
|
826 |
+
else:
|
827 |
+
self.b_polyak_mean.value = self.polyak_mean
|
828 |
+
|
829 |
+
def on_b_polyak_std(self, value):
|
830 |
+
val = value['new']
|
831 |
+
if val >= 0 and val <= 1:
|
832 |
+
self.polyak_std = val
|
833 |
+
else:
|
834 |
+
self.b_polyak_std.value = self.polyak_std
|
835 |
+
|
836 |
+
def on_b_epsilon(self, value):
|
837 |
+
val = value['new']
|
838 |
+
if val > 0 and val < 0.1:
|
839 |
+
self.epsilon = val
|
840 |
+
else:
|
841 |
+
self.b_epsilon.value = self.epsilon
|
842 |
+
|
843 |
def on_b_filter(self, value):
|
844 |
val = value['new']
|
845 |
self.filter = val
|
|
|
848 |
val = value['new']
|
849 |
self.record = val
|
850 |
|
851 |
+
def on_b_lsl(self, value):
|
852 |
+
val = value['new']
|
853 |
+
self.lsl = val
|
854 |
+
|
855 |
def on_b_display(self, value):
|
856 |
val = value['new']
|
857 |
self.display = val
|
|
|
895 |
def start_capture(self,
|
896 |
filter,
|
897 |
record,
|
898 |
+
lsl,
|
899 |
viz,
|
900 |
width,
|
901 |
python_clock):
|
|
|
907 |
p_msg_io, p_msg_io_2 = mp.Pipe()
|
908 |
p_data_i, p_data_o = mp.Pipe(duplex=False)
|
909 |
SAMPLE_TIME = 1 / self.frequency
|
910 |
+
|
911 |
+
if filter:
|
912 |
+
fp = FilterPipeline(nb_channels=8,
|
913 |
+
sampling_rate=self.frequency,
|
914 |
+
power_line_fq=self.power_line,
|
915 |
+
use_custom_fir=False,
|
916 |
+
custom_fir_order=10,
|
917 |
+
custom_fir_cutoff=30,
|
918 |
+
alpha_avg=0.1,
|
919 |
+
alpha_std=0.001,
|
920 |
+
epsilon=0.000001)
|
921 |
+
|
922 |
self._p_capture = mp.Process(target=_capture_process,
|
923 |
args=(p_data_o,
|
924 |
p_msg_io_2,
|
|
|
936 |
|
937 |
if record:
|
938 |
self.open_recording_file()
|
939 |
+
|
940 |
+
if lsl:
|
941 |
+
from pylsl import StreamInfo, StreamOutlet
|
942 |
+
lsl_info = StreamInfo(name='Portiloop',
|
943 |
+
type='EEG',
|
944 |
+
channel_count=8,
|
945 |
+
channel_format='float32',
|
946 |
+
source_id='') # TODO: replace this by unique device identifier
|
947 |
+
lsl_outlet = StreamOutlet(lsl_info)
|
948 |
|
949 |
buffer = []
|
950 |
|
|
|
972 |
|
973 |
if filter:
|
974 |
n_array = fp.filter(n_array)
|
|
|
|
|
|
|
975 |
|
976 |
+
filtered_point = n_array.tolist()
|
977 |
+
|
978 |
+
if lsl:
|
979 |
+
lsl_outlet.push_sample(filtered_point[-1])
|
980 |
+
|
981 |
+
buffer += filtered_point
|
982 |
if len(buffer) >= 50:
|
983 |
|
984 |
if viz:
|
portiloop/notebooks/test_LSL.ipynb
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"id": "37a4d718",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [
|
9 |
+
{
|
10 |
+
"name": "stderr",
|
11 |
+
"output_type": "stream",
|
12 |
+
"text": [
|
13 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'lo' (status: 0, multicast: 1, broadcast: 0)\n",
|
14 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'eth0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
15 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'wlan0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
16 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'p2p0' (status: 4096, multicast: 0, broadcast: 2)\n",
|
17 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'lo' (status: 0, multicast: 1, broadcast: 0)\n",
|
18 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'eth0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
19 |
+
"2022-03-25 20:14:39.506 ( 0.018s) [python3 ] netinterfaces.cpp:102 INFO| \tIPv4 addr: c0a80172\n",
|
20 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'wlan0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
21 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:102 INFO| \tIPv4 addr: c0a80001\n",
|
22 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'lo' (status: 0, multicast: 1, broadcast: 0)\n",
|
23 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'eth0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
24 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:105 INFO| \tIPv6 addr: fd24:dec0:e89c::b8b\n",
|
25 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'eth0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
26 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:105 INFO| \tIPv6 addr: fd24:dec0:e89c:0:57e3:6122:3c71:7ca4\n",
|
27 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'eth0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
28 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:105 INFO| \tIPv6 addr: fe80::1d2b:5fd6:c05d:9642%eth0\n",
|
29 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:91 INFO| netif 'wlan0' (status: 4096, multicast: 1, broadcast: 2)\n",
|
30 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] netinterfaces.cpp:105 INFO| \tIPv6 addr: fe80::7ed9:5cff:feb2:5133%wlan0\n",
|
31 |
+
"2022-03-25 20:14:39.507 ( 0.018s) [python3 ] api_config.cpp:270 INFO| Loaded default config\n",
|
32 |
+
"2022-03-25 20:14:39.509 ( 0.020s) [python3 ] common.cpp:65 INFO| v1.15.2-108-g6cdcf74d\n",
|
33 |
+
"2022-03-25 20:14:39.509 ( 0.021s) [python3 ] udp_server.cpp:82 WARN| Could not bind multicast responder for ff02:113d:6fdd:2c17:a643:ffe2:1bd1:3cd2 to interface fd24:dec0:e89c:0:57e3:6122:3c71:7ca4 (Address already in use)\n",
|
34 |
+
"2022-03-25 20:14:39.509 ( 0.021s) [python3 ] udp_server.cpp:82 WARN| Could not bind multicast responder for ff02:113d:6fdd:2c17:a643:ffe2:1bd1:3cd2 to interface fe80::7ed9:5cff:feb2:5133%wlan0 (Address already in use)\n",
|
35 |
+
"2022-03-25 20:14:39.509 ( 0.021s) [python3 ] udp_server.cpp:82 WARN| Could not bind multicast responder for ff05:113d:6fdd:2c17:a643:ffe2:1bd1:3cd2 to interface fd24:dec0:e89c:0:57e3:6122:3c71:7ca4 (Address already in use)\n",
|
36 |
+
"2022-03-25 20:14:39.510 ( 0.021s) [python3 ] udp_server.cpp:82 WARN| Could not bind multicast responder for ff05:113d:6fdd:2c17:a643:ffe2:1bd1:3cd2 to interface fe80::7ed9:5cff:feb2:5133%wlan0 (Address already in use)\n"
|
37 |
+
]
|
38 |
+
},
|
39 |
+
{
|
40 |
+
"name": "stdout",
|
41 |
+
"output_type": "stream",
|
42 |
+
"text": [
|
43 |
+
"now sending data...\n"
|
44 |
+
]
|
45 |
+
},
|
46 |
+
{
|
47 |
+
"ename": "KeyboardInterrupt",
|
48 |
+
"evalue": "",
|
49 |
+
"output_type": "error",
|
50 |
+
"traceback": [
|
51 |
+
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
52 |
+
"\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
|
53 |
+
"\u001b[0;32m/tmp/ipykernel_3906/1992969341.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 42\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0.01\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 43\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 44\u001b[0;31m \u001b[0mmain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
|
54 |
+
"\u001b[0;32m/tmp/ipykernel_3906/1992969341.py\u001b[0m in \u001b[0;36mmain\u001b[0;34m()\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[0msent_samples\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mrequired_samples\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 41\u001b[0m \u001b[0;31m# now send it and wait for a bit before trying again.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 42\u001b[0;31m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0.01\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 43\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 44\u001b[0m \u001b[0mmain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
55 |
+
"\u001b[0;31mKeyboardInterrupt\u001b[0m: "
|
56 |
+
]
|
57 |
+
}
|
58 |
+
],
|
59 |
+
"source": [
|
60 |
+
"\"\"\"Example program to demonstrate how to send a multi-channel time series to\n",
|
61 |
+
"LSL.\"\"\"\n",
|
62 |
+
"import sys\n",
|
63 |
+
"import getopt\n",
|
64 |
+
"\n",
|
65 |
+
"import time\n",
|
66 |
+
"from random import random as rand\n",
|
67 |
+
"\n",
|
68 |
+
"from pylsl import StreamInfo, StreamOutlet, local_clock\n",
|
69 |
+
"\n",
|
70 |
+
"\n",
|
71 |
+
"def main():\n",
|
72 |
+
" srate = 100\n",
|
73 |
+
" name = 'BioSemi'\n",
|
74 |
+
" type = 'EEG'\n",
|
75 |
+
" n_channels = 8\n",
|
76 |
+
"\n",
|
77 |
+
" # first create a new stream info (here we set the name to BioSemi,\n",
|
78 |
+
" # the content-type to EEG, 8 channels, 100 Hz, and float-valued data) The\n",
|
79 |
+
" # last value would be the serial number of the device or some other more or\n",
|
80 |
+
" # less locally unique identifier for the stream as far as available (you\n",
|
81 |
+
" # could also omit it but interrupted connections wouldn't auto-recover)\n",
|
82 |
+
" info = StreamInfo(name, type, n_channels, srate, 'float32', 'myuid34234')\n",
|
83 |
+
"\n",
|
84 |
+
" # next make an outlet\n",
|
85 |
+
" outlet = StreamOutlet(info)\n",
|
86 |
+
"\n",
|
87 |
+
" print(\"now sending data...\")\n",
|
88 |
+
" start_time = local_clock()\n",
|
89 |
+
" sent_samples = 0\n",
|
90 |
+
" while True:\n",
|
91 |
+
" elapsed_time = local_clock() - start_time\n",
|
92 |
+
" required_samples = int(srate * elapsed_time) - sent_samples\n",
|
93 |
+
" for sample_ix in range(required_samples):\n",
|
94 |
+
" # make a new random n_channels sample; this is converted into a\n",
|
95 |
+
" # pylsl.vectorf (the data type that is expected by push_sample)\n",
|
96 |
+
" mysample = [rand() for _ in range(n_channels)]\n",
|
97 |
+
" # now send it\n",
|
98 |
+
" outlet.push_sample(mysample)\n",
|
99 |
+
" sent_samples += required_samples\n",
|
100 |
+
" # now send it and wait for a bit before trying again.\n",
|
101 |
+
" time.sleep(0.01)\n",
|
102 |
+
"\n",
|
103 |
+
"main()"
|
104 |
+
]
|
105 |
+
},
|
106 |
+
{
|
107 |
+
"cell_type": "code",
|
108 |
+
"execution_count": null,
|
109 |
+
"id": "e67e094c",
|
110 |
+
"metadata": {},
|
111 |
+
"outputs": [],
|
112 |
+
"source": []
|
113 |
+
}
|
114 |
+
],
|
115 |
+
"metadata": {
|
116 |
+
"kernelspec": {
|
117 |
+
"display_name": "Python 3 (ipykernel)",
|
118 |
+
"language": "python",
|
119 |
+
"name": "python3"
|
120 |
+
},
|
121 |
+
"language_info": {
|
122 |
+
"codemirror_mode": {
|
123 |
+
"name": "ipython",
|
124 |
+
"version": 3
|
125 |
+
},
|
126 |
+
"file_extension": ".py",
|
127 |
+
"mimetype": "text/x-python",
|
128 |
+
"name": "python",
|
129 |
+
"nbconvert_exporter": "python",
|
130 |
+
"pygments_lexer": "ipython3",
|
131 |
+
"version": "3.7.3"
|
132 |
+
}
|
133 |
+
},
|
134 |
+
"nbformat": 4,
|
135 |
+
"nbformat_minor": 5
|
136 |
+
}
|
portiloop/notebooks/tests.ipynb
CHANGED
@@ -2,17 +2,40 @@
|
|
2 |
"cells": [
|
3 |
{
|
4 |
"cell_type": "code",
|
5 |
-
"execution_count":
|
6 |
"id": "7b2fc5da",
|
7 |
"metadata": {
|
8 |
"scrolled": false
|
9 |
},
|
10 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
"source": [
|
12 |
"from portiloop.capture import Capture\n",
|
13 |
"\n",
|
14 |
"cap = Capture()"
|
15 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
}
|
17 |
],
|
18 |
"metadata": {
|
|
|
2 |
"cells": [
|
3 |
{
|
4 |
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
"id": "7b2fc5da",
|
7 |
"metadata": {
|
8 |
"scrolled": false
|
9 |
},
|
10 |
+
"outputs": [
|
11 |
+
{
|
12 |
+
"data": {
|
13 |
+
"application/vnd.jupyter.widget-view+json": {
|
14 |
+
"model_id": "695e7a12068640caa478d92b47a6e76c",
|
15 |
+
"version_major": 2,
|
16 |
+
"version_minor": 0
|
17 |
+
},
|
18 |
+
"text/plain": [
|
19 |
+
"VBox(children=(Accordion(children=(GridBox(children=(Label(value='CH1'), Label(value='CH2'), Label(value='CH3'…"
|
20 |
+
]
|
21 |
+
},
|
22 |
+
"metadata": {},
|
23 |
+
"output_type": "display_data"
|
24 |
+
}
|
25 |
+
],
|
26 |
"source": [
|
27 |
"from portiloop.capture import Capture\n",
|
28 |
"\n",
|
29 |
"cap = Capture()"
|
30 |
]
|
31 |
+
},
|
32 |
+
{
|
33 |
+
"cell_type": "code",
|
34 |
+
"execution_count": null,
|
35 |
+
"id": "a34e9672",
|
36 |
+
"metadata": {},
|
37 |
+
"outputs": [],
|
38 |
+
"source": []
|
39 |
}
|
40 |
],
|
41 |
"metadata": {
|
setup.py
CHANGED
@@ -12,6 +12,8 @@ setup(
|
|
12 |
'portilooplot',
|
13 |
'ipywidgets',
|
14 |
'python-periphery',
|
15 |
-
'spidev'
|
|
|
|
|
16 |
]
|
17 |
)
|
|
|
12 |
'portilooplot',
|
13 |
'ipywidgets',
|
14 |
'python-periphery',
|
15 |
+
'spidev',
|
16 |
+
'pylsl-coral',
|
17 |
+
'scipy'
|
18 |
]
|
19 |
)
|