msobral commited on
Commit
ce1d18e
·
1 Parent(s): 56adc7e

first complete version (no bias)

Browse files
portiloop/capture.py CHANGED
@@ -1,19 +1,17 @@
1
  from time import sleep
2
  import time
3
- from playsound import playsound
4
  import numpy as np
5
  import matplotlib.pyplot as plt
6
  import os
7
  from pathlib import Path
8
  from datetime import datetime, timedelta
9
  import multiprocessing as mp
10
- from queue import Empty
11
  import warnings
12
  import shutil
13
- from pyedflib import highlevel
14
- from datetime import datetime
15
 
16
  import matplotlib.pyplot as plt
 
17
 
18
  from portilooplot.jupyter_plot import ProgressPlot
19
  from portiloop.hardware.frontend import Frontend
@@ -82,6 +80,8 @@ FRONTEND_CONFIG = [
82
  0x20, # Enable SRB1
83
  ]
84
 
 
 
85
  def mod_config(config, datarate):
86
  possible_datarates = [(250, 0x06),
87
  (500, 0x05),
@@ -99,23 +99,20 @@ def mod_config(config, datarate):
99
  new_cf1 = config[1] & 0xF8
100
  new_cf1 = new_cf1 | j
101
  config[1] = new_cf1
102
- print(f"DEBUG: new cf1: {hex(config[1])}")
103
  return config
104
 
105
  def filter_24(value):
106
  return (value * 4.5) / (2**23 - 1) # 23 because 1 bit is lost for sign
107
 
108
  def filter_2scomplement_np(value):
109
- v = np.where((value & (1 << 23)) != 0, value - (1 << 24), value)
110
- return filter_24(v)
 
 
111
 
112
  class LiveDisplay():
113
- def __init__(self, datapoint_dim=8, window_len=100):
114
- self.datapoint_dim = datapoint_dim
115
- self.queue = mp.Queue()
116
- channel_names = [f"channel#{i+1}" for i in range(datapoint_dim)]
117
- channel_names[0] = "voltage"
118
- channel_names[7] = "temperature"
119
  self.pp = ProgressPlot(plot_names=channel_names, max_window_len=window_len)
120
 
121
  def add_datapoints(self, datapoints):
@@ -136,14 +133,15 @@ class LiveDisplay():
136
  self.pp.update(disp_list)
137
 
138
 
139
- def _capture_process(q_data, q_out, q_in, duration, frequency, python_clock=True):
140
  """
141
  Args:
142
- q_data: multiprocessing.Queue: captured datapoints are put in the queue
143
- q_out: mutliprocessing.Queue: to pass messages to the parent process
144
- 'STOP': end of the the process
145
- q_in: mutliprocessing.Queue: to pass messages from the parent process
146
- 'STOP': stops the process
 
147
  """
148
  if duration <= 0:
149
  duration = np.inf
@@ -191,9 +189,10 @@ def _capture_process(q_data, q_out, q_in, duration, frequency, python_clock=True
191
  # first sample:
192
  reading = frontend.read()
193
  datapoint = reading.channels()
194
- q_data.put(datapoint)
195
 
196
  t_next = t + sample_time
 
197
 
198
  # sampling loop:
199
  while c and t < t_max:
@@ -206,46 +205,52 @@ def _capture_process(q_data, q_out, q_in, duration, frequency, python_clock=True
206
  else:
207
  reading = frontend.wait_new_data()
208
  datapoint = reading.channels()
209
- q_data.put(datapoint)
210
-
211
-
212
- # Check for messages # this takes too long :/
213
- # try:
214
- # message = q_in.get_nowait()
215
- # if message == 'STOP':
216
- # c = False
217
- # except Empty:
218
- # pass
219
  it += 1
220
  t = time.time()
221
  tot = (t - t_start) / it
222
 
223
- print(f"Average frequency: {1 / tot} Hz for {it} samples")
224
 
225
  leds.aquisition(False)
226
 
227
  finally:
228
  frontend.close()
229
  leds.close()
230
- q_in.close()
231
- q_out.put('STOP')
 
232
 
233
 
234
  class Capture:
235
  def __init__(self):
236
-
237
- self.filename = Path.home() / 'edf_recording' / f"recording_{now.strftime('%m_%d_%Y_%H_%M_%S')}.edf"
238
  self._p_capture = None
239
  self.__capture_on = False
240
  self.frequency = 250
241
  self.duration = 10
242
  self.record = False
243
  self.display = False
244
- self.recording_file = None
245
  self.python_clock = True
246
-
247
- self.binfile = None
248
- self.temp_path = Path.home() / '.temp'
 
 
 
 
 
 
 
249
 
250
  # widgets
251
 
@@ -269,9 +274,8 @@ class Capture:
269
  )
270
 
271
  self.b_filename = widgets.Text(
272
- value=self.filename,
273
- description='Filename:',
274
- placeholder='All files will be in the edf_recording folder'
275
  disabled=False
276
  )
277
 
@@ -317,23 +321,53 @@ class Capture:
317
  def display_buttons(self):
318
  display(widgets.VBox([self.b_frequency,
319
  self.b_duration,
 
320
  widgets.HBox([self.b_record, self.b_display]),
321
  self.b_clock,
322
  self.b_capture]))
 
 
 
 
 
 
 
 
323
 
 
 
 
 
 
 
 
 
324
  def on_b_capture(self, value):
325
  val = value['new']
326
  if val == 'Start':
327
- self.start_capture(
328
- record=self.record,
329
- viz=self.display,
330
- width=500,
331
- python_clock=self.python_clock)
332
- elif val == 'Stop':
333
  clear_output()
 
334
  self.display_buttons()
335
- else:
336
- print(f"This option is not supported: {val}.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  def on_b_clock(self, value):
339
  val = value['new']
@@ -341,30 +375,26 @@ class Capture:
341
  self.python_clock = True
342
  elif val == 'ADS':
343
  self.python_clock = False
344
- else:
345
- print(f"This option is not supported: {val}.")
346
 
347
  def on_b_frequency(self, value):
348
  val = value['new']
349
  if val > 0:
350
  self.frequency = val
351
- else:
352
- print(f"Unsupported frequency: {val} Hz")
353
 
354
  def on_b_filename(self, value):
355
  val = value['new']
356
  if val != '':
357
- self.filename = Path.home() / 'edf_recording' / val
 
 
358
  else:
359
  now = datetime.now()
360
- self.filename = Path.home() / 'edf_recording' / f"recording_{now.strftime('%m_%d_%Y_%H_%M_%S')}.edf"
361
 
362
  def on_b_duration(self, value):
363
  val = value['new']
364
  if val > 0:
365
  self.duration = val
366
- else:
367
- print(f"Unsupported duration: {val} s")
368
 
369
  def on_b_record(self, value):
370
  val = value['new']
@@ -375,110 +405,123 @@ class Capture:
375
  self.display = val
376
 
377
  def open_recording_file(self):
 
 
 
 
 
 
378
  print(f"Will store edf recording in {self.filename}")
379
- os.mkdir(self.temp_path)
380
- self.binfile = open(self.temp_path / 'data.bin', 'wb')
381
-
382
- def close_recording_file(self):
383
-
384
- print('Saving recording data...')
385
- # Channel names
386
- channels = ['Voltage', 'Ch2', 'Ch3', 'Ch4', 'Ch5', 'Ch6', 'Ch7', 'Temperature']
387
-
388
- # Read binary data
389
- data = np.fromfile(self.temp_path / 'data.bin', dtype=float)
390
- data = data.reshape((8, int(data.shape[0]/8)))
391
-
392
- # Declare and write EDF format file
393
- signal_headers = highlevel.make_signal_headers(channels, sample_frequency=self.frequency)
394
- header = highlevel.make_header(patientname='patient_x', gender='Female')
395
- highlevel.write_edf(self.filename, data, signal_headers, header)
396
-
397
- # Close and delete temp binary file
398
- self.binfile.close()
399
- shutil.rmtree(self.temp_path)
400
 
401
- print('...done')
 
 
 
 
 
 
 
 
 
 
402
 
403
  def add_recording_data(self, data):
404
- np.array(data).tofile(self.binfile)
 
 
 
 
 
 
 
 
405
 
406
  def start_capture(self,
407
- record=True,
408
- viz=False,
409
- width=500,
410
- python_clock=True):
411
- self.q_messages_send = mp.Queue()
412
- self.q_messages_recv = mp.Queue()
413
- self.q_data = mp.Queue()
414
 
415
  if self.__capture_on:
416
- print("Capture is already ongoing, ignoring command.")
417
  return
418
  else:
419
  self.__capture_on = True
420
- SAMPLE_TIME = 1 / frequency
421
- self._p_capture = mp.Process(target=_capture_process, args=(self.q_data,
422
- self.q_messages_recv,
423
- self.q_messages_send,
424
- self.duration,
425
- self.frequency,
426
- python_clock))
 
427
  self._p_capture.start()
428
-
429
  if viz:
430
- live_disp = LiveDisplay(window_len=width)
431
-
432
  if record:
433
  self.open_recording_file()
434
 
435
  cc = True
436
  while cc:
437
- try:
438
- mess = self.q_messages_recv.get_nowait()
 
 
 
 
439
  if mess == 'STOP':
440
  cc = False
441
- except Empty:
442
- pass
443
 
444
- # retrieve all data points from q_data and put them in a list of np.array:
445
  res = []
446
  c = True
447
  while c and len(res) < 25:
448
- try:
449
- point = self.q_data.get(timeout=SAMPLE_TIME)
450
  res.append(point)
451
- except Empty:
452
  c = False
453
  if len(res) == 0:
454
  continue
 
455
  n_array = np.array(res)
456
- n_array = filter_2scomplement_np(n_array)
457
-
458
  to_add = n_array.tolist()
459
-
460
  if viz:
461
  live_disp.add_datapoints(to_add)
462
  if record:
463
  self.add_recording_data(to_add)
464
-
465
- # empty q_data
466
  cc = True
467
  while cc:
468
- try:
469
- _ = self.q_data.get_nowait()
470
- except Empty:
 
 
471
  cc = False
472
 
473
- self.q_messages_recv.close()
474
- self.q_data.close()
475
 
476
  if record:
477
  self.close_recording_file()
478
 
479
- # print("DEBUG: joining capture process...")
480
  self._p_capture.join()
481
- # print("DEBUG: capture process joined.")
482
  self.__capture_on = False
483
 
484
 
 
1
  from time import sleep
2
  import time
 
3
  import numpy as np
4
  import matplotlib.pyplot as plt
5
  import os
6
  from pathlib import Path
7
  from datetime import datetime, timedelta
8
  import multiprocessing as mp
 
9
  import warnings
10
  import shutil
11
+ 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
 
80
  0x20, # Enable SRB1
81
  ]
82
 
83
+ EDF_PATH = Path.home() / 'workspace' / 'edf_recording'
84
+
85
  def mod_config(config, datarate):
86
  possible_datarates = [(250, 0x06),
87
  (500, 0x05),
 
99
  new_cf1 = config[1] & 0xF8
100
  new_cf1 = new_cf1 | j
101
  config[1] = new_cf1
 
102
  return config
103
 
104
  def filter_24(value):
105
  return (value * 4.5) / (2**23 - 1) # 23 because 1 bit is lost for sign
106
 
107
  def filter_2scomplement_np(value):
108
+ return np.where((value & (1 << 23)) != 0, value - (1 << 24), value)
109
+
110
+ def filter_np(value):
111
+ return filter_24(filter_2scomplement_np(value))
112
 
113
  class LiveDisplay():
114
+ def __init__(self, channel_names, window_len=100):
115
+ self.datapoint_dim = len(channel_names)
 
 
 
 
116
  self.pp = ProgressPlot(plot_names=channel_names, max_window_len=window_len)
117
 
118
  def add_datapoints(self, datapoints):
 
133
  self.pp.update(disp_list)
134
 
135
 
136
+ def _capture_process(p_data_o, p_msg_io, duration, frequency, python_clock=True, time_msg_in=1.0):
137
  """
138
  Args:
139
+ p_data_o: multiprocessing.Pipe: captured datapoints are put here
140
+ p_msg_io: mutliprocessing.Pipe: to communicate with the parent process
141
+ duration: float: max duration of the experiment in seconds
142
+ frequency: float: sampling frequency
143
+ ptyhon_clock: bool (default True): if True, the Coral clock is used, otherwise, the ADS interrupts are used
144
+ time_msg_in: float (default 1.0): min time between attempts to recv incomming messages
145
  """
146
  if duration <= 0:
147
  duration = np.inf
 
189
  # first sample:
190
  reading = frontend.read()
191
  datapoint = reading.channels()
192
+ p_data_o.send(datapoint)
193
 
194
  t_next = t + sample_time
195
+ t_chk_msg = t + time_msg_in
196
 
197
  # sampling loop:
198
  while c and t < t_max:
 
205
  else:
206
  reading = frontend.wait_new_data()
207
  datapoint = reading.channels()
208
+ p_data_o.send(datapoint)
209
+
210
+ # Check for messages
211
+ if t >= t_chk_msg:
212
+ t_chk_msg = t + time_msg_in
213
+ if p_msg_io.poll():
214
+ message = p_msg_io.recv()
215
+ if message == 'STOP':
216
+ c = False
 
217
  it += 1
218
  t = time.time()
219
  tot = (t - t_start) / it
220
 
221
+ p_msg_io.send(("PRT", f"Average frequency: {1 / tot} Hz for {it} samples"))
222
 
223
  leds.aquisition(False)
224
 
225
  finally:
226
  frontend.close()
227
  leds.close()
228
+ p_msg_io.send('STOP')
229
+ p_msg_io.close()
230
+ p_data_o.close()
231
 
232
 
233
  class Capture:
234
  def __init__(self):
235
+ # {now.strftime('%m_%d_%Y_%H_%M_%S')}
236
+ self.filename = EDF_PATH / 'recording.edf'
237
  self._p_capture = None
238
  self.__capture_on = False
239
  self.frequency = 250
240
  self.duration = 10
241
  self.record = False
242
  self.display = False
 
243
  self.python_clock = True
244
+ self.edf_writer = None
245
+ self.edf_buffer = []
246
+ self.nb_signals = 8
247
+ self.samples_per_datarecord_array = self.frequency
248
+ self.physical_max = 5
249
+ self.physical_min = -5
250
+ self.signal_labels = ['voltage', 'ch2', 'ch3', 'ch4', 'ch5', 'ch6', 'ch7', 'temperature']
251
+ self._lock_msg_out = Lock()
252
+ self._msg_out = None
253
+ self._t_capture = None
254
 
255
  # widgets
256
 
 
274
  )
275
 
276
  self.b_filename = widgets.Text(
277
+ value='recording.edf',
278
+ description='Recording:',
 
279
  disabled=False
280
  )
281
 
 
321
  def display_buttons(self):
322
  display(widgets.VBox([self.b_frequency,
323
  self.b_duration,
324
+ self.b_filename,
325
  widgets.HBox([self.b_record, self.b_display]),
326
  self.b_clock,
327
  self.b_capture]))
328
+
329
+ def enable_buttons(self):
330
+ self.b_frequency.disabled = False
331
+ self.b_duration.disabled = False
332
+ self.b_filename.disabled = False
333
+ self.b_record.disabled = False
334
+ self.b_display.disabled = False
335
+ self.b_clock.disabled = False
336
 
337
+ def disable_buttons(self):
338
+ self.b_frequency.disabled = True
339
+ self.b_duration.disabled = True
340
+ self.b_filename.disabled = True
341
+ self.b_record.disabled = True
342
+ self.b_display.disabled = True
343
+ self.b_clock.disabled = True
344
+
345
  def on_b_capture(self, value):
346
  val = value['new']
347
  if val == 'Start':
 
 
 
 
 
 
348
  clear_output()
349
+ self.disable_buttons()
350
  self.display_buttons()
351
+ with self._lock_msg_out:
352
+ self._msg_out = None
353
+ if self._t_capture is not None:
354
+ warnings.warn("Capture already running, operation aborted.")
355
+ return
356
+ self._t_capture = Thread(target=self.start_capture,
357
+ args=(self.record, self.display, 500, self.python_clock))
358
+ self._t_capture.start()
359
+ # self.start_capture(
360
+ # record=self.record,
361
+ # viz=self.display,
362
+ # width=500,
363
+ # python_clock=self.python_clock)
364
+ elif val == 'Stop':
365
+ with self._lock_msg_out:
366
+ self._msg_out = 'STOP'
367
+ assert self._t_capture is not None
368
+ self._t_capture.join()
369
+ self._t_capture = None
370
+ self.enable_buttons()
371
 
372
  def on_b_clock(self, value):
373
  val = value['new']
 
375
  self.python_clock = True
376
  elif val == 'ADS':
377
  self.python_clock = False
 
 
378
 
379
  def on_b_frequency(self, value):
380
  val = value['new']
381
  if val > 0:
382
  self.frequency = val
 
 
383
 
384
  def on_b_filename(self, value):
385
  val = value['new']
386
  if val != '':
387
+ if not val.endswith('.edf'):
388
+ val += '.edf'
389
+ self.filename = EDF_PATH / val
390
  else:
391
  now = datetime.now()
392
+ self.filename = EDF_PATH / 'recording.edf'
393
 
394
  def on_b_duration(self, value):
395
  val = value['new']
396
  if val > 0:
397
  self.duration = val
 
 
398
 
399
  def on_b_record(self, value):
400
  val = value['new']
 
405
  self.display = val
406
 
407
  def open_recording_file(self):
408
+ nb_signals = self.nb_signals
409
+ samples_per_datarecord_array = self.samples_per_datarecord_array
410
+ physical_max = self.physical_max
411
+ physical_min = self.physical_min
412
+ signal_labels = self.signal_labels
413
+
414
  print(f"Will store edf recording in {self.filename}")
415
+
416
+ self.edf_writer = EDFwriter(p_path=str(self.filename),
417
+ f_file_type=EDFwriter.EDFLIB_FILETYPE_EDFPLUS,
418
+ number_of_signals=nb_signals)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
+ for signal in range(nb_signals):
421
+ assert self.edf_writer.setSampleFrequency(signal, samples_per_datarecord_array) == 0
422
+ assert self.edf_writer.setPhysicalMaximum(signal, physical_max) == 0
423
+ assert self.edf_writer.setPhysicalMinimum(signal, physical_min) == 0
424
+ assert self.edf_writer.setDigitalMaximum(signal, 32767) == 0
425
+ assert self.edf_writer.setDigitalMinimum(signal, -32768) == 0
426
+ assert self.edf_writer.setSignalLabel(signal, signal_labels[signal]) == 0
427
+ assert self.edf_writer.setPhysicalDimension(signal, 'V') == 0
428
+
429
+ def close_recording_file(self):
430
+ assert self.edf_writer.close() == 0
431
 
432
  def add_recording_data(self, data):
433
+ self.edf_buffer += data
434
+ if len(self.edf_buffer) >= self.samples_per_datarecord_array:
435
+ datarecord_array = self.edf_buffer[:self.samples_per_datarecord_array]
436
+ self.edf_buffer = self.edf_buffer[self.samples_per_datarecord_array:]
437
+ datarecord_array = np.array(datarecord_array).transpose()
438
+ assert len(datarecord_array) == self.nb_signals, f"len(data)={len(data)}!={self.nb_signals}"
439
+ for d in datarecord_array:
440
+ assert len(d) == self.samples_per_datarecord_array, f"{len(d)}!={self.samples_per_datarecord_array}"
441
+ assert self.edf_writer.writeSamples(d) == 0
442
 
443
  def start_capture(self,
444
+ record,
445
+ viz,
446
+ width,
447
+ python_clock):
448
+
449
+ p_msg_io, p_msg_io_2 = mp.Pipe()
450
+ p_data_i, p_data_o = mp.Pipe(duplex=False)
451
 
452
  if self.__capture_on:
453
+ warnings.warn("Capture is already ongoing, ignoring command.")
454
  return
455
  else:
456
  self.__capture_on = True
457
+ SAMPLE_TIME = 1 / self.frequency
458
+ self._p_capture = mp.Process(target=_capture_process,
459
+ args=(p_data_o,
460
+ p_msg_io_2,
461
+ self.duration,
462
+ self.frequency,
463
+ python_clock)
464
+ )
465
  self._p_capture.start()
466
+
467
  if viz:
468
+ live_disp = LiveDisplay(channel_names = self.signal_labels, window_len=width)
469
+
470
  if record:
471
  self.open_recording_file()
472
 
473
  cc = True
474
  while cc:
475
+ with self._lock_msg_out:
476
+ if self._msg_out is not None:
477
+ p_msg_io.send(self._msg_out)
478
+ self._msg_out = None
479
+ if p_msg_io.poll():
480
+ mess = p_msg_io.recv()
481
  if mess == 'STOP':
482
  cc = False
483
+ elif mess[0] == 'PRT':
484
+ print(mess[1])
485
 
486
+ # retrieve all data points from p_data and put them in a list of np.array:
487
  res = []
488
  c = True
489
  while c and len(res) < 25:
490
+ if p_data_i.poll(timeout=SAMPLE_TIME):
491
+ point = p_data_i.recv()
492
  res.append(point)
493
+ else:
494
  c = False
495
  if len(res) == 0:
496
  continue
497
+
498
  n_array = np.array(res)
499
+ n_array = filter_np(n_array)
500
+
501
  to_add = n_array.tolist()
502
+
503
  if viz:
504
  live_disp.add_datapoints(to_add)
505
  if record:
506
  self.add_recording_data(to_add)
507
+
508
+ # empty pipes
509
  cc = True
510
  while cc:
511
+ if p_data_i.poll():
512
+ _ = p_data_i.recv()
513
+ elif p_msg_io.poll():
514
+ _ = p_msg_io.recv()
515
+ else:
516
  cc = False
517
 
518
+ p_data_i.close()
519
+ p_msg_io.close()
520
 
521
  if record:
522
  self.close_recording_file()
523
 
 
524
  self._p_capture.join()
 
525
  self.__capture_on = False
526
 
527
 
portiloop/notebooks/test_capture.ipynb DELETED
The diff for this file is too large to render. See raw diff
 
portiloop/notebooks/tests.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
setup.py CHANGED
@@ -5,8 +5,13 @@ setup(
5
  version='0.0.1',
6
  packages=[package for package in find_packages()],
7
  description='Library for portiloop',
8
- install_requires=['numpy',
9
- 'matplotlib',
10
- 'portilooplot',
11
- 'ipywidgets']
 
 
 
 
 
12
  )
 
5
  version='0.0.1',
6
  packages=[package for package in find_packages()],
7
  description='Library for portiloop',
8
+ install_requires=['wheel',
9
+ 'EDFlib-Python',
10
+ 'numpy',
11
+ 'matplotlib',
12
+ 'portilooplot',
13
+ 'ipywidgets',
14
+ 'python-periphery',
15
+ 'spidev'
16
+ ]
17
  )