ybouteiller commited on
Commit
c9c81f5
·
1 Parent(s): 15952ff

new interface, lsl, custom filtering

Browse files
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 | j
104
  config[1] = new_cf1
105
 
106
  # bias:
@@ -178,9 +193,18 @@ class FIR:
178
 
179
 
180
  class FilterPipeline:
181
- def __init__(self, nb_channels, power_line_fq=60):
 
 
 
 
 
 
 
 
 
182
  self.nb_channels = nb_channels
183
- assert power_line_fq in [50, 60], f"The only supported power line frequencies are 50Hz and 60Hz"
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 = 0.1
201
- self.ALPHA_STD = 0.001
202
- self.EPSILON = 0.000001
203
-
204
- self.fir_30_coef = [
205
- 0.001623780150148094927192721215192250384,
206
- 0.014988684599373741992978104065059596905,
207
- 0.021287595318265635502275046064823982306,
208
- 0.007349500393709578957568417933998716762,
209
- -0.025127515717112181709014251396183681209,
210
- -0.052210507359822452833064687638398027048,
211
- -0.039273839505489904766477593511808663607,
212
- 0.033021568427940004020193498490698402748,
213
- 0.147606943281569008563636202779889572412,
214
- 0.254000252034505602516389899392379447818,
215
- 0.297330876398883392486283128164359368384,
216
- 0.254000252034505602516389899392379447818,
217
- 0.147606943281569008563636202779889572412,
218
- 0.033021568427940004020193498490698402748,
219
- -0.039273839505489904766477593511808663607,
220
- -0.052210507359822452833064687638398027048,
221
- -0.025127515717112181709014251396183681209,
222
- 0.007349500393709578957568417933998716762,
223
- 0.021287595318265635502275046064823982306,
224
- 0.014988684599373741992978104065059596905,
225
- 0.001623780150148094927192721215192250384]
226
- self.fir = FIR(self.nb_channels, self.fir_30_coef)
 
 
 
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
- # icons=['check'] * 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  )
493
 
494
  self.b_filename = widgets.Text(
@@ -498,33 +549,83 @@ class Capture:
498
  )
499
 
500
  self.b_frequency = widgets.IntText(
501
- value=250,
502
  description='Freq (Hz):',
503
  disabled=False
504
  )
505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  self.b_duration = widgets.IntText(
507
- value=10,
508
  description='Time (s):',
509
  disabled=False
510
  )
511
 
512
  self.b_filter = widgets.Checkbox(
513
- value=True,
514
  description='Filter',
515
  disabled=False,
516
  indent=False
517
  )
518
 
519
  self.b_record = widgets.Checkbox(
520
- value=False,
521
- description='Record',
 
 
 
 
 
 
 
522
  disabled=False,
523
  indent=False
524
  )
525
 
526
  self.b_display = widgets.Checkbox(
527
- value=False,
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
- widgets.HBox([self.b_filter, self.b_record, self.b_display]),
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
- fp = FilterPipeline(nb_channels=8)
 
 
 
 
 
 
 
 
 
 
 
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
- buffer += n_array.tolist()
 
 
 
 
 
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": null,
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
  )