From b92ad51794c93a09b97c1f52ded78012ca1ca5a6 Mon Sep 17 00:00:00 2001 From: lawhead Date: Fri, 5 Jan 2024 16:16:52 -0800 Subject: [PATCH 1/2] #186164071 ; added active/passive status to devices --- bcipy/acquisition/devices.py | 37 ++++++++++-- bcipy/acquisition/multimodal.py | 9 +++ bcipy/acquisition/protocols/lsl/lsl_client.py | 1 + bcipy/acquisition/tests/test_devices.py | 60 +++++++++++++++---- bcipy/helpers/acquisition.py | 44 ++++++++++---- bcipy/helpers/tests/test_acquisition.py | 16 +++-- bcipy/parameters/devices.json | 15 +++-- bcipy/signal/model/offline_analysis.py | 32 +++++----- bcipy/task/paradigm/rsvp/copy_phrase.py | 2 +- .../tests/paradigm/rsvp/test_copy_phrase.py | 1 + 10 files changed, 164 insertions(+), 53 deletions(-) diff --git a/bcipy/acquisition/devices.py b/bcipy/acquisition/devices.py index 12a2c1f56..4eb194df9 100644 --- a/bcipy/acquisition/devices.py +++ b/bcipy/acquisition/devices.py @@ -2,8 +2,9 @@ devices.""" import json import logging +from enum import Enum, auto from pathlib import Path -from typing import Optional, Dict, List, NamedTuple, Union +from typing import Dict, List, NamedTuple, Optional, Union from bcipy.config import DEFAULT_ENCODING, DEVICE_SPEC_PATH @@ -50,6 +51,22 @@ def channel_spec(channel: Union[str, dict, ChannelSpec]) -> ChannelSpec: raise Exception("Unexpected channel type") +class DeviceStatus(Enum): + """Represents the recording status of a device during acquisition.""" + ACTIVE = auto() + PASSIVE = auto() + + def __str__(self) -> str: + """String representation""" + return self.name.lower() + + @classmethod + def from_str(cls, name: str) -> 'DeviceStatus': + """Returns the DeviceStatus associated with the given string + representation.""" + return cls[name.upper()] + + class DeviceSpec: """Specification for a hardware device used in data acquisition. @@ -67,6 +84,7 @@ class DeviceSpec: data_type - data format of a channel; all channels must have the same type; see https://labstreaminglayer.readthedocs.io/projects/liblsl/ref/enums.html excluded_from_analysis - list of channels (label) to exclude from analysis. + status - recording status """ def __init__(self, @@ -76,7 +94,8 @@ def __init__(self, content_type: str = DEFAULT_DEVICE_TYPE, description: Optional[str] = None, excluded_from_analysis: Optional[List[str]] = None, - data_type='float32'): + data_type: str = 'float32', + status: DeviceStatus = DeviceStatus.ACTIVE): assert sample_rate >= 0, "Sample rate can't be negative." assert data_type in SUPPORTED_DATA_TYPES @@ -89,6 +108,7 @@ def __init__(self, self.data_type = data_type self.excluded_from_analysis = excluded_from_analysis or [] self._validate_excluded_channels() + self.status = status @property def channel_count(self) -> int: @@ -117,6 +137,12 @@ def analysis_channels(self) -> List[str]: filter(lambda channel: channel not in self.excluded_from_analysis, self.channels)) + @property + def is_active(self) -> bool: + """Returns a boolean indicating if the device is currently active + (recording status set to DeviceStatus.ACTIVE).""" + return self.status == DeviceStatus.ACTIVE + def to_dict(self) -> dict: """Converts the DeviceSpec to a dict.""" return { @@ -125,7 +151,8 @@ def to_dict(self) -> dict: 'channels': [ch._asdict() for ch in self.channel_specs], 'sample_rate': self.sample_rate, 'description': self.description, - 'excluded_from_analysis': self.excluded_from_analysis + 'excluded_from_analysis': self.excluded_from_analysis, + 'status': str(self.status) } def __str__(self): @@ -153,13 +180,15 @@ def _validate_excluded_channels(self): def make_device_spec(config: dict) -> DeviceSpec: """Constructs a DeviceSpec from a dict. Throws a KeyError if any fields are missing.""" + default_status = str(DeviceStatus.ACTIVE) return DeviceSpec(name=config['name'], content_type=config['content_type'], channels=config['channels'], sample_rate=config['sample_rate'], description=config['description'], excluded_from_analysis=config.get( - 'excluded_from_analysis', [])) + 'excluded_from_analysis', []), + status=DeviceStatus.from_str(config.get('status', default_status))) def load(config_path: Path = Path(DEFAULT_CONFIG), replace: bool = False) -> Dict[str, DeviceSpec]: diff --git a/bcipy/acquisition/multimodal.py b/bcipy/acquisition/multimodal.py index 43b3ca560..8249f0885 100644 --- a/bcipy/acquisition/multimodal.py +++ b/bcipy/acquisition/multimodal.py @@ -84,6 +84,15 @@ def device_content_types(self) -> List[ContentType]: """ return list(self._clients.keys()) + @property + def active_device_content_types(self) -> List[ContentType]: + """Returns a list of ContentTypes provided by the active configured + devices.""" + return [ + content_type for content_type, spec in self._clients.items() + if spec.is_active + ] + @property def default_client(self) -> Optional[LslAcquisitionClient]: """Returns the default client.""" diff --git a/bcipy/acquisition/protocols/lsl/lsl_client.py b/bcipy/acquisition/protocols/lsl/lsl_client.py index f82d545b9..c78f64b90 100644 --- a/bcipy/acquisition/protocols/lsl/lsl_client.py +++ b/bcipy/acquisition/protocols/lsl/lsl_client.py @@ -127,6 +127,7 @@ def stop_acquisition(self) -> None: log.info("Closing LSL connection") self.inlet.close_stream() self.inlet = None + log.info("Inlet closed") self.buffer = None diff --git a/bcipy/acquisition/tests/test_devices.py b/bcipy/acquisition/tests/test_devices.py index 31c0a766c..aedaea775 100644 --- a/bcipy/acquisition/tests/test_devices.py +++ b/bcipy/acquisition/tests/test_devices.py @@ -6,7 +6,6 @@ from pathlib import Path from bcipy.acquisition import devices - from bcipy.config import DEFAULT_ENCODING @@ -21,8 +20,8 @@ def test_default_supported_devices(self): """List of supported devices should include generic values for backwards compatibility.""" supported = devices.preconfigured_devices() - self.assertTrue(len(supported.keys()) > 0) - self.assertTrue('DSI-24' in supported.keys()) + self.assertTrue(len(supported) > 0) + self.assertTrue('DSI-24' in supported) dsi = supported['DSI-24'] self.assertEqual('EEG', dsi.content_type) @@ -49,13 +48,15 @@ def test_load_from_config(self): devices.load(config_path, replace=True) supported = devices.preconfigured_devices() - self.assertEqual(1, len(supported.keys())) - self.assertTrue('DSI-VR300' in supported.keys()) + self.assertEqual(1, len(supported)) + self.assertTrue('DSI-VR300' in supported) spec = supported['DSI-VR300'] self.assertEqual('EEG', spec.content_type) self.assertEqual(300.0, spec.sample_rate) self.assertEqual(channels, spec.channels) + self.assertEqual(devices.DeviceStatus.ACTIVE, spec.status) + self.assertTrue(spec.is_active) self.assertEqual(spec, devices.preconfigured_device('DSI-VR300')) shutil.rmtree(temp_dir) @@ -89,14 +90,14 @@ def test_load_channel_specs_from_config(self): with open(config_path, 'w', encoding=DEFAULT_ENCODING) as config_file: json.dump(my_devices, config_file) - prior_device_count = len(devices.preconfigured_devices().keys()) + prior_device_count = len(devices.preconfigured_devices()) devices.load(config_path) supported = devices.preconfigured_devices() spec = supported["Custom-Device"] self.assertEqual(spec.channels, ['Fz', 'Pz', 'F7']) self.assertEqual(spec.channel_names, ['ch1', 'ch2', 'ch3']) - self.assertEqual(len(supported.keys()), prior_device_count + 1) + self.assertEqual(len(supported), prior_device_count + 1) shutil.rmtree(temp_dir) def test_load_missing_config(self): @@ -120,16 +121,16 @@ def test_device_registration(self): sample_rate=300.0, description="Custom built device") supported = devices.preconfigured_devices() - device_count = len(supported.keys()) + device_count = len(supported) self.assertTrue(device_count > 0) - self.assertTrue('my-device' not in supported.keys()) + self.assertTrue('my-device' not in supported) spec = devices.make_device_spec(data) devices.register(spec) supported = devices.preconfigured_devices() - self.assertEqual(device_count + 1, len(supported.keys())) - self.assertTrue('my-device' in supported.keys()) + self.assertEqual(device_count + 1, len(supported)) + self.assertTrue('my-device' in supported) def test_search_by_name(self): """Should be able to find a supported device by name.""" @@ -143,6 +144,7 @@ def test_device_spec_defaults(self): sample_rate=256.0) self.assertEqual(3, spec.channel_count) self.assertEqual('EEG', spec.content_type) + self.assertEqual(devices.DeviceStatus.ACTIVE, spec.status) def test_device_spec_analysis_channels(self): """DeviceSpec should have a list of channels used for analysis.""" @@ -228,12 +230,46 @@ def test_device_spec_to_dict(self): spec = devices.DeviceSpec(name=device_name, channels=channels, sample_rate=sample_rate, - content_type=content_type) + content_type=content_type, + status=devices.DeviceStatus.PASSIVE) spec_dict = spec.to_dict() self.assertEqual(device_name, spec_dict['name']) self.assertEqual(content_type, spec_dict['content_type']) self.assertEqual(expected_channel_output, spec_dict['channels']) self.assertEqual(sample_rate, spec_dict['sample_rate']) + self.assertEqual('passive', spec_dict['status']) + + def test_load_status(self): + """Should be able to load a list of supported devices from a + configuration file.""" + + # create a config file in a temp location. + temp_dir = tempfile.mkdtemp() + my_devices = [ + dict(name="MyDevice", + content_type="EEG", + description="My Device", + channels=["a", "b", "c"], + sample_rate=100.0, + status=str(devices.DeviceStatus.PASSIVE)) + ] + config_path = Path(temp_dir, 'my_devices.json') + with open(config_path, 'w', encoding=DEFAULT_ENCODING) as config_file: + json.dump(my_devices, config_file) + + devices.load(config_path, replace=True) + supported = devices.preconfigured_devices() + self.assertEqual(devices.DeviceStatus.PASSIVE, supported['MyDevice'].status) + shutil.rmtree(temp_dir) + + def test_device_status(self): + """Test DeviceStatus enum""" + self.assertEqual('active', str(devices.DeviceStatus.ACTIVE)) + self.assertEqual(devices.DeviceStatus.ACTIVE, + devices.DeviceStatus.from_str('active')) + self.assertEqual( + devices.DeviceStatus.PASSIVE, + devices.DeviceStatus.from_str(str(devices.DeviceStatus.PASSIVE))) if __name__ == '__main__': diff --git a/bcipy/helpers/acquisition.py b/bcipy/helpers/acquisition.py index c129470d3..0a92c9b7a 100644 --- a/bcipy/helpers/acquisition.py +++ b/bcipy/helpers/acquisition.py @@ -2,15 +2,15 @@ import logging import subprocess import time -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import numpy as np from bcipy.acquisition import (ClientManager, LslAcquisitionClient, LslDataServer, await_start, discover_device_spec) -from bcipy.acquisition.devices import (DeviceSpec, preconfigured_device, - with_content_type) +from bcipy.acquisition.devices import (DeviceSpec, DeviceStatus, + preconfigured_device, with_content_type) from bcipy.config import BCIPY_ROOT from bcipy.config import DEFAULT_DEVICE_SPEC_FILENAME as spec_name from bcipy.config import RAW_DATA_FILENAME @@ -52,7 +52,7 @@ def init_eeg_acquisition( manager = ClientManager() for stream_type in stream_types(parameters['acq_mode']): - content_type, device_name = parse_stream_type(stream_type) + content_type, device_name, status = parse_stream_type(stream_type) if server: server_device_spec = server_spec(content_type, device_name) @@ -64,6 +64,8 @@ def init_eeg_acquisition( await_start(dataserver) device_spec = init_device(content_type, device_name) + if status: + device_spec.status = status raw_data_name = raw_data_filename(device_spec) client = init_lsl_client(parameters, device_spec, save_folder, @@ -138,24 +140,44 @@ def server_spec(content_type: str, return devices[0] +class StreamType(NamedTuple): + """Specifies a stream.""" + content_type: str + device_name: Optional[str] = None + status: Optional[DeviceStatus] = None + + def parse_stream_type(stream_type: str, - delimiter: str = "/") -> Tuple[str, Optional[str]]: - """Parses the stream type into a tuple of (content_type, device_name). + name_delim: str = "/", + status_delim: str = ":") -> StreamType: + """Parses the stream type into a tuple of (content_type, device_name, status). Parameters ---------- stream_type - LSL content type (EEG, Gaze, etc). If you know the name of the preconfigured device it can be added 'EEG/DSI-24' - + name_delim - optionally used after the content_type (and status) + for specifying the device name. + status_delim - optionally used after the content_type to specify the + status for the device ('active' or 'passive') >>> parse_stream_type('EEG/DSI-24') ('EEG', 'DSI-24') >>> parse_stream_type('Gaze') ('Gaze', None) + >>> parse_stream_type('Gaze:passive') + ('Gaze', None, DeviceStatus.PASSIVE) """ - if delimiter in stream_type: - content_type, device_name = stream_type.split(delimiter)[0:2] - return (content_type, device_name) - return (stream_type, None) + device_name = None + status = None + + if name_delim in stream_type: + content_type, device_name = stream_type.split(name_delim)[0:2] + else: + content_type = stream_type + if status_delim in content_type: + content_type, status_str = content_type.split(status_delim)[0:2] + status = DeviceStatus.from_str(status_str) + return StreamType(content_type, device_name, status) def init_lsl_client(parameters: dict, diff --git a/bcipy/helpers/tests/test_acquisition.py b/bcipy/helpers/tests/test_acquisition.py index 3ec07e6ed..22a725c9b 100644 --- a/bcipy/helpers/tests/test_acquisition.py +++ b/bcipy/helpers/tests/test_acquisition.py @@ -4,7 +4,7 @@ from pathlib import Path from unittest.mock import Mock, patch -from bcipy.acquisition.devices import DeviceSpec +from bcipy.acquisition.devices import DeviceSpec, DeviceStatus from bcipy.config import DEFAULT_PARAMETERS_PATH from bcipy.helpers.acquisition import (RAW_DATA_FILENAME, init_device, init_eeg_acquisition, @@ -39,7 +39,7 @@ def test_init_acquisition(self): """Test init_eeg_acquisition with LSL client.""" params = self.parameters - params['acq_mode'] = 'EEG/DSI-24' + params['acq_mode'] = 'EEG:passive/DSI-24' client, servers = init_eeg_acquisition(params, self.save, server=True) @@ -50,6 +50,7 @@ def test_init_acquisition(self): self.assertEqual(1, len(servers)) self.assertEqual(client.device_spec.name, 'DSI-24') + self.assertFalse(client.device_spec.is_active) self.assertTrue(Path(self.save, 'devices.json').is_file()) @@ -145,9 +146,14 @@ def test_init_device_with_named_device(self, preconfigured_device_mock, self.assertEqual(device_spec, device_mock) def test_parse_stream_type(self): - """Test function to split the stream type into content_type, name""" - self.assertEqual(('EEG', 'DSI-24'), parse_stream_type('EEG/DSI-24')) - self.assertEqual(('Gaze', None), parse_stream_type('Gaze')) + """Test function to split the stream type into content_type, name, + and status""" + self.assertEqual(('EEG', 'DSI-24', None), parse_stream_type('EEG/DSI-24')) + self.assertEqual(('Gaze', None, None), parse_stream_type('Gaze')) + self.assertEqual(('Gaze', None, DeviceStatus.PASSIVE), + parse_stream_type('Gaze:passive')) + self.assertEqual(('EEG', 'DSI-24', DeviceStatus.ACTIVE), + parse_stream_type('EEG:active/DSI-24')) @patch('bcipy.helpers.acquisition.preconfigured_device') def test_server_spec_with_named_device(self, preconfigured_device_mock): diff --git a/bcipy/parameters/devices.json b/bcipy/parameters/devices.json index 01e4724b8..c9254b966 100644 --- a/bcipy/parameters/devices.json +++ b/bcipy/parameters/devices.json @@ -40,7 +40,8 @@ "P3", "P4", "F3", "F4", "C3", "C4" - ] + ], + "status": "active" }, { "name": "DSI-VR300", @@ -57,7 +58,8 @@ ], "sample_rate": 300, "description": "Wearable Sensing DSI-VR300", - "excluded_from_analysis": ["TRG", "F7"] + "excluded_from_analysis": ["TRG", "F7"], + "status": "active" }, { "name": "DSI-Flex", @@ -74,7 +76,8 @@ ], "sample_rate": 300.0, "description": "Wearable Sensing DSI-Flex", - "excluded_from_analysis": ["TRG"] + "excluded_from_analysis": ["TRG"], + "status": "active" }, { "name": "g.USBamp-1", @@ -98,7 +101,8 @@ { "name": "Ch16", "label": "Ch16", "units": "microvolts", "type": "EEG" } ], "sample_rate": 256, - "description": "GTec g.USBamp" + "description": "GTec g.USBamp", + "status": "active" }, { "name": "Tobii Nano", @@ -115,6 +119,7 @@ ], "sample_rate": 60.0, "description": "Tobii Nano. For use with the Tobii Pro SDK.", - "excluded_from_analysis": ["device_ts", "system_ts", "left_pupil", "right_pupil"] + "excluded_from_analysis": ["device_ts", "system_ts", "left_pupil", "right_pupil"], + "status": "active" } ] \ No newline at end of file diff --git a/bcipy/signal/model/offline_analysis.py b/bcipy/signal/model/offline_analysis.py index b5ed089b5..9144dc7c0 100644 --- a/bcipy/signal/model/offline_analysis.py +++ b/bcipy/signal/model/offline_analysis.py @@ -1,9 +1,9 @@ # mypy: disable-error-code="attr-defined" # needed for the ERPTransformParams +import json import logging from pathlib import Path from typing import Tuple -import json import numpy as np from matplotlib.figure import Figure @@ -11,10 +11,9 @@ from sklearn.model_selection import train_test_split import bcipy.acquisition.devices as devices -from bcipy.config import (DEFAULT_DEVICE_SPEC_FILENAME, BCIPY_ROOT, - DEFAULT_PARAMETERS_PATH, STATIC_AUDIO_PATH, - TRIGGER_FILENAME, - MATRIX_IMAGE_FILENAME) +from bcipy.config import (BCIPY_ROOT, DEFAULT_DEVICE_SPEC_FILENAME, + DEFAULT_PARAMETERS_PATH, MATRIX_IMAGE_FILENAME, + STATIC_AUDIO_PATH, TRIGGER_FILENAME) from bcipy.helpers.acquisition import analysis_channels, raw_data_filename from bcipy.helpers.load import (load_experimental_data, load_json_parameters, load_raw_data) @@ -24,14 +23,15 @@ from bcipy.helpers.symbols import alphabet from bcipy.helpers.system_utils import report_execution_time from bcipy.helpers.triggers import TriggerType, trigger_decoder -from bcipy.helpers.visualization import (visualize_erp, visualize_gaze, +from bcipy.helpers.visualization import (visualize_centralized_data, + visualize_erp, visualize_gaze, + visualize_gaze_accuracies, visualize_gaze_inquiries, - visualize_centralized_data, - visualize_results_all_symbols, - visualize_gaze_accuracies) + visualize_results_all_symbols) from bcipy.preferences import preferences from bcipy.signal.model.base_model import SignalModel, SignalModelMetadata -from bcipy.signal.model.gaussian_mixture import GazeModelIndividual, GazeModelCombined +from bcipy.signal.model.gaussian_mixture import (GazeModelCombined, + GazeModelIndividual) from bcipy.signal.model.pca_rda_kde import PcaRdaKdeModel from bcipy.signal.process import (ERPTransformParams, extract_eye_info, filter_inquiries, get_default_transform) @@ -473,6 +473,7 @@ def offline_analysis( pickle dumps the model into a .pkl folder How it Works: + For every active device that was used during calibration, - reads data and information from a .csv calibration file - reads trigger information from a .txt trigger file - filters data @@ -506,11 +507,12 @@ def offline_analysis( devices_by_name = devices.load( Path(data_folder, DEFAULT_DEVICE_SPEC_FILENAME), replace=True) - data_file_paths = [ - path for path in (Path(data_folder, raw_data_filename(device_spec)) - for device_spec in devices_by_name.values()) - if path.exists() - ] + + active_devices = (spec for spec in devices_by_name.values() + if spec.is_active) + active_raw_data_paths = (Path(data_folder, raw_data_filename(device_spec)) + for device_spec in active_devices) + data_file_paths = [path for path in active_raw_data_paths if path.exists()] models = [] figure_handles = [] diff --git a/bcipy/task/paradigm/rsvp/copy_phrase.py b/bcipy/task/paradigm/rsvp/copy_phrase.py index 1cf0712f2..cb3b44810 100644 --- a/bcipy/task/paradigm/rsvp/copy_phrase.py +++ b/bcipy/task/paradigm/rsvp/copy_phrase.py @@ -190,7 +190,7 @@ def init_evidence_evaluators(self, evaluator = init_evidence_evaluator(self.alp, model) content_type = evaluator.consumes evidence_type = evaluator.produces - if content_type in self.daq.device_content_types: + if content_type in self.daq.active_device_content_types: evaluators.append(evaluator) if evidence_type in evidence_types: raise DuplicateModelEvidence( diff --git a/bcipy/task/tests/paradigm/rsvp/test_copy_phrase.py b/bcipy/task/tests/paradigm/rsvp/test_copy_phrase.py index 68fb1e794..7c4b97b3b 100644 --- a/bcipy/task/tests/paradigm/rsvp/test_copy_phrase.py +++ b/bcipy/task/tests/paradigm/rsvp/test_copy_phrase.py @@ -102,6 +102,7 @@ def setUp(self): 'is_calibrated': True, 'offset': lambda x: 0.0, 'device_content_types': [ContentType.EEG], + 'active_device_content_types': [ContentType.EEG], 'clients_by_type': { ContentType.EEG: self.eeg_client_mock } From 272b0ee2d1b85bee7407b6e3a9b1e5fd45efffb0 Mon Sep 17 00:00:00 2001 From: lawhead Date: Fri, 5 Jan 2024 16:45:00 -0800 Subject: [PATCH 2/2] Added documentation --- bcipy/acquisition/README.md | 19 +++++++++++++++++++ bcipy/parameters/parameters.json | 5 +++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/bcipy/acquisition/README.md b/bcipy/acquisition/README.md index 4e8060c9d..6fa441155 100644 --- a/bcipy/acquisition/README.md +++ b/bcipy/acquisition/README.md @@ -17,6 +17,8 @@ Within BciPy users must specify the details of the device they wish to use by pr `DeviceSpec` channel data can be provided as a list of channel names or specified as a list of `ChannelSpec` entries. These will be validated against the metadata from the LSL data stream. If provided, `ChannelSpecs` allow users to customize the channel labels and override the values provided by the LSL metadata. +Devices can also have a status of either 'active' or 'passive'. Active devices are used for training models (after calibration) and for interactive queries during Copy Phrase tasks. Passive devices are used for recording data in the background for possible later analysis. Device status can be set in the `devices.json` file or through the parameters. + ## Client The `lsl_client` module provides the primary interface for interacting with device data and may be used to dynamically query streaming data in real-time. An instance of the `LslClient` class is parameterized with the `DeviceSpec` of interest, as well as the number of seconds of data that should be available for querying. If the `save_directory` and `filename` parameters are provided it also records the data to disk for later offline analysis. If no device is specified the client will attempt to connect to an EEG stream by default. A separate `LslClient` should be constructed for each device of interest. @@ -70,3 +72,20 @@ Generators are Python functions that yield encoded data. They have a parameter f #### Producer A `Producer` is a class internal to a server that manages the generation of data at a specified frequency. It's purpose it to mimic the data rate that would be presented if an actual hardware device was used. + +## Acquisition Parameters + +The primary parameter for configuring the acquisition module for running an experiment is the `acq_mode` parameter. This parameter allows users to specify one or more devices from which to acquire data, as well as the status of each device. In its simplest form the parameter is a string specifying the content type of the LSL data stream of interest (for example. 'EEG'). The first detected device with this content type will be used. Alternatively, users can provide the device name ('EEG/DSI-24'), which specifies that we are interested in a specific device. The module will look for the corresponding data stream, and will use configuration for the device from the `devices.json` file. Finally, users can specify whether the provided device will be recording in active or passive mode ('EEG:passive/DSI-24'). One or more devices can be configured by delimiting the specs with a '+' ('EEG/DSI-24+Eyetracker:passive'). + +### Use Cases + +- I want to Calibrate using EEG and Eyetracker and train models for both. + - set parameter `'acq_mode': 'EEG+Eyetracker'` +- I want to Calibrate using EEG and Eyetracker and train models for EEG only. + - set parameter `'acq_mode': 'EEG+Eyetracker:passive'` +- I want to Calibrate using EEG and Eyetracker and train models for Eyetracker only. + - set parameter `'acq_mode': 'EEG:passive+Eyetracker'` +- I want to do a Copy Phrase task using EEG only. In my calibration directory I have trained models for both EEG and Eyetracker. + - set parameter `'acq_mode': 'EEG'` +- I want to do a Copy Phrase task using EEG only, but I want to record gaze data. In my calibration directory I have trained models for both EEG and Eyetracker. + - set parameter `'acq_mode': 'EEG+Eyetracker:passive'` \ No newline at end of file diff --git a/bcipy/parameters/parameters.json b/bcipy/parameters/parameters.json index 724b343ac..b17e6f8c5 100755 --- a/bcipy/parameters/parameters.json +++ b/bcipy/parameters/parameters.json @@ -11,12 +11,13 @@ "value": "EEG", "section": "acq_config", "readableName": "Acquisition Mode", - "helpTip": "Specifies the hardware device(s) used for data collection. Default: EEG", + "helpTip": "Specifies the hardware device(s) used for data collection. Default: EEG.", "recommended_values": [ "EEG", "EEG/DSI-24", "Eyetracker", - "EEG+Eyetracker" + "EEG+Eyetracker", + "EEG+Eyetracker:passive" ], "type": "str" },