From 2da217e2d2eb44ee7eabed04512d6d892899add4 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Wed, 27 Nov 2024 16:16:33 +0000 Subject: [PATCH 01/11] Olivier's changes --- iblrig/net.py | 10 ++++++++-- iblrig/path_helper.py | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/iblrig/net.py b/iblrig/net.py index 738a5c865..5b385c776 100644 --- a/iblrig/net.py +++ b/iblrig/net.py @@ -123,6 +123,11 @@ def __init__(self, clients): if any(self._clients): self._thread = threading.Thread(target=asyncio.run, args=(self.listen(),), name='network_coms') self._thread.start() + log.debug('waiting for connection with timeout 1 sec') + with self.connected: + success = self.connected.wait_for(lambda: self.is_connected is True, timeout=1) + if not success: + raise RuntimeError('Failed to connect to remote rigs') @property def is_connected(self) -> bool: @@ -196,7 +201,8 @@ async def listen(self): are sent to the remote services and the collated responses are added to the log. Exits only after stop event is set. This should be called from a daemon thread. """ - await self.create() + with self.connected: + await self.create() while not self.stop_event.is_set(): if any(queue := sorted(self._queued)): request_time = queue[0] @@ -222,7 +228,7 @@ async def listen(self): # yield x # # res = await anext(first(asyncio.as_completed(tasks), lambda r: r[-1]['main_sync'])) - responses = await self.services.info(event, *args) + responses = await self.services.info(*args) case net.base.ExpMessage.ALYX: responses = await self.services.alyx(*args) case _: diff --git a/iblrig/path_helper.py b/iblrig/path_helper.py index 535d46ec8..3b603b8d4 100644 --- a/iblrig/path_helper.py +++ b/iblrig/path_helper.py @@ -145,8 +145,10 @@ def get_local_and_remote_paths( 'remote_subjects_folder': PosixPath('Y:/Subjects')} """ # we only want to attempt to load the settings file if necessary - if (local_path is None) or (remote_path is None) or (lab is None): - iblrig_settings = load_pydantic_yaml(RigSettings) if iblrig_settings is None else iblrig_settings + if iblrig_settings is None and ((local_path is None) or (remote_path is None) or (lab is None)): + iblrig_settings = load_pydantic_yaml(RigSettings) + if isinstance(iblrig_settings, RigSettings): + iblrig_settings = iblrig_settings.model_dump() paths = Bunch({'local_data_folder': local_path, 'remote_data_folder': remote_path}) if paths.local_data_folder is None: From f7311901a5f6fbb09da29e1823a37d71aae0b857 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 28 Nov 2024 16:48:53 +0000 Subject: [PATCH 02/11] Load settings method --- iblrig/base_tasks.py | 23 +++++++++++++---------- iblrig/video.py | 5 +++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 1ba71d104..5f99f6a05 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -130,15 +130,7 @@ def __init__( self.init_datetime = datetime.datetime.now() # loads in the settings: first load the files, then update with the input argument if provided - self.hardware_settings: HardwareSettings = load_pydantic_yaml(HardwareSettings, file_hardware_settings) - if hardware_settings is not None: - self.hardware_settings.update(hardware_settings) - HardwareSettings.model_validate(self.hardware_settings) - self.iblrig_settings: RigSettings = load_pydantic_yaml(RigSettings, file_iblrig_settings) - if iblrig_settings is not None: - self.iblrig_settings.update(iblrig_settings) - RigSettings.model_validate(self.iblrig_settings) - + self._load_settings(file_hardware_settings=file_hardware_settings, hardware_settings=hardware_settings, file_iblrig_settings=file_iblrig_settings, iblrig_settings=iblrig_settings) self.wizard = wizard # Load the tasks settings, from the task folder or override with the input argument @@ -174,6 +166,16 @@ def __init__( extractors=self.extractor_tasks, ) + def _load_settings(self, file_hardware_settings=None, hardware_settings=None, file_iblrig_settings=None, iblrig_settings=None, **_): + self.hardware_settings: HardwareSettings = load_pydantic_yaml(HardwareSettings, file_hardware_settings) + if hardware_settings is not None: + self.hardware_settings.update(hardware_settings) + HardwareSettings.model_validate(self.hardware_settings) + self.iblrig_settings: RigSettings = load_pydantic_yaml(RigSettings, file_iblrig_settings) + if iblrig_settings is not None: + self.iblrig_settings.update(iblrig_settings) + RigSettings.model_validate(self.iblrig_settings) + @classmethod def get_task_file(cls) -> Path: """ @@ -1208,7 +1210,8 @@ def __init__(self, *_, remote_rigs=None, **kwargs): if isinstance(remote_rigs, list): # For now we flatten to list of remote rig names but could permit list of (name, URI) tuples remote_rigs = list(filter(None, flatten(remote_rigs))) - all_remote_rigs = net.get_remote_devices(iblrig_settings=kwargs.get('iblrig_settings')) + self._load_settings(**kwargs) + all_remote_rigs = net.get_remote_devices(iblrig_settings=self.iblrig_settings) if not set(remote_rigs).issubset(all_remote_rigs.keys()): raise ValueError('Selected remote rigs not in remote rigs list') remote_rigs = {k: v for k, v in all_remote_rigs.items() if k in remote_rigs} diff --git a/iblrig/video.py b/iblrig/video.py index cdc312963..e0ee69669 100644 --- a/iblrig/video.py +++ b/iblrig/video.py @@ -775,7 +775,8 @@ async def _process_keyboard_input(self, line): async def on_init(self, data, addr): """Process init command from remote rig.""" self.logger.info('INIT message received') - assert (exp_ref := (data or {}).get('exp_ref')), 'No experiment reference found' + data = data[0] if any(data) else {} + assert (exp_ref := data.get('exp_ref')), 'No experiment reference found' # FIXME graceful error (stop thread somehow so there's no hanging) if isinstance(exp_ref, str): exp_ref = self.one.ref2dict(exp_ref) # NB: Only the first match case for which predicate is true will be run so we can update the status dynamically @@ -807,7 +808,7 @@ async def on_init(self, data, addr): case _: raise NotImplementedError(f'Unexpected status "{self.status}"') data = ExpInfo(self.exp_ref, False, self.experiment_description) - await self.communicator.init(self.status, data.to_dict(), addr=addr) + await self.communicator.init([self.status, data.to_dict()], addr=addr) async def on_start(self, data, addr): """Process init command from remote rig.""" From 31ae898a3be7465374f64ecfe8983b6b88426fcb Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 28 Nov 2024 17:29:57 +0000 Subject: [PATCH 03/11] Ignore append for now --- iblrig/base_tasks.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 5f99f6a05..927e1fbd5 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -1299,11 +1299,12 @@ def _init_paths(self, append: bool = False): assert self.exp_ref paths.SESSION_FOLDER = date_folder / f'{self.exp_ref["sequence"]:03}' paths.TASK_COLLECTION = iblrig.path_helper.iterate_collection(paths.SESSION_FOLDER) - if append == paths.TASK_COLLECTION.endswith('00'): - raise ValueError( - f'Append value incorrect. Either remove previous task collections from ' - f'{paths.SESSION_FOLDER}, or select append in GUI (--append arg in cli)' - ) + # if append == paths.TASK_COLLECTION.endswith('00'): + # raise ValueError( + # f'Append value incorrect. Either remove previous task collections from ' + # f'{paths.SESSION_FOLDER}, or select append in GUI (--append arg in cli)' + # ) + log.critical('This is task number %i for %s', int(paths.TASK_COLLECTION.split('_')[-1]) + 1, self.exp_ref) paths.SESSION_RAW_DATA_FOLDER = paths.SESSION_FOLDER.joinpath(paths.TASK_COLLECTION) paths.DATA_FILE_PATH = paths.SESSION_RAW_DATA_FOLDER.joinpath('_iblrig_taskData.raw.jsonable') @@ -1402,6 +1403,7 @@ def init_mixin_network(self): f'Running past or future sessions not currently supported. \n' f'Please check the system date time settings on each rig.' ) + # TODO How to handle folder already existing before running UDP experiment? # exp_ref = ConversionMixin.path2ref(self.paths['SESSION_FOLDER'], as_dict=False) exp_ref = self.one.dict2ref(self.exp_ref) From 88d73b0eb21a888bb055241f89b1cd92867281ff Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 29 Nov 2024 15:23:36 +0000 Subject: [PATCH 04/11] Stop and finalize video --- iblrig/video.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/iblrig/video.py b/iblrig/video.py index e0ee69669..c12b55bf5 100644 --- a/iblrig/video.py +++ b/iblrig/video.py @@ -709,6 +709,7 @@ async def run(self, service_uri=None): else: self.logger.info('Bonsai camera acquisition stopped') self._status = net.base.ExpStatus.STOPPED + self.finalize_recording() # TODO We could send a message to remote here case _: raise NotImplementedError(f'Unexpected task "{task.get_name()}"') @@ -756,8 +757,13 @@ async def _process_keyboard_input(self, line): return self.logger.info('Received keyboard event: %s', line) match line: + case 'STOP': + self.stop_recording() case 'QUIT': self.communicator.close() + process_finished = self.bonsai_process and self.bonsai_process.returncode is not None + if not (self.status is net.base.ExpStatus.STOPPED and process_finished): + self.stop_recording() case line if line.startswith('QUIT!'): self.close() case 'START': From a94adadbd786df120f0221432bb19db686f5ef7e Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 29 Nov 2024 15:26:34 +0000 Subject: [PATCH 05/11] Use CameraSession class --- iblrig/video.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iblrig/video.py b/iblrig/video.py index c12b55bf5..65bab430b 100644 --- a/iblrig/video.py +++ b/iblrig/video.py @@ -191,15 +191,17 @@ def prepare_video_session_cmd(): if args.subject_name is None and args.service_uri is False: parser.error('--subject-name is mandatory if --service-uri has not been provided.') - setup_logger(name='iblrig', level='DEBUG' if args.debug else 'INFO') + log_level = 'DEBUG' if args.debug else 'INFO' + setup_logger(name='iblrig', level=log_level) service_uri = args.service_uri # Technically `prepare_video_service` should behave the same as `prepare_video_session` if the service_uri arg is # False but until fully tested, let's call the old function if service_uri is False: # TODO Use CameraSession object and remove prepare_video_session and prepare_video_service - prepare_video_session(args.subject_name, args.profile, debug=args.debug) + # prepare_video_session(args.subject_name, args.profile, debug=args.debug) + session = CameraSession(subject=args.subject_name, config_name=args.profile, log_level=log_level) + session.run() else: - log_level = 'DEBUG' if args.debug else 'INFO' session = CameraSessionNetworked(subject=args.subject_name, config_name=args.profile, log_level=log_level) asyncio.run(session.run(service_uri)) From 449ca6829445ae5de4472ad65c0706bac869b32e Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 29 Nov 2024 16:03:51 +0000 Subject: [PATCH 06/11] returncode instead of poll is compatible with sync and async processes --- iblrig/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iblrig/video.py b/iblrig/video.py index 65bab430b..427a1f6f0 100644 --- a/iblrig/video.py +++ b/iblrig/video.py @@ -596,7 +596,7 @@ def finalize_recording(self): os.removedirs(self.paths['SESSION_RAW_DATA_FOLDER']) def stop_recording(self): - if self.bonsai_process and self.bonsai_process.poll() is None: + if self.bonsai_process and self.bonsai_process.returncode is None: self.bonsai_process.terminate() self._status = net.base.ExpStatus.STOPPED self.logger.info('Video acquisition session finished.') From a1c262c41f0e89c5217d773440f8c134af252dd9 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 29 Nov 2024 16:32:48 +0000 Subject: [PATCH 07/11] Await after terminate --- iblrig/video.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/iblrig/video.py b/iblrig/video.py index 427a1f6f0..c7b047ab5 100644 --- a/iblrig/video.py +++ b/iblrig/video.py @@ -669,6 +669,14 @@ def start_recording(self): assert task and not task.done(), 'No Bonsai process found!' return super().start_recording() + async def stop_recording(self): + if self.bonsai_process and self.bonsai_process.returncode is None: + self.bonsai_process.terminate() + await self.bonsai_process.wait() + self._status = net.base.ExpStatus.STOPPED + self.logger.info('Video acquisition session finished.') + self.finalize_recording() + @property def is_connected(self) -> bool: """bool: True if communicator is connected.""" @@ -760,12 +768,12 @@ async def _process_keyboard_input(self, line): self.logger.info('Received keyboard event: %s', line) match line: case 'STOP': - self.stop_recording() + await self.stop_recording() case 'QUIT': self.communicator.close() process_finished = self.bonsai_process and self.bonsai_process.returncode is not None if not (self.status is net.base.ExpStatus.STOPPED and process_finished): - self.stop_recording() + await self.stop_recording() case line if line.startswith('QUIT!'): self.close() case 'START': From f8908703c5a739c0d8315705f1e645604707548e Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Mon, 2 Dec 2024 13:41:35 +0000 Subject: [PATCH 08/11] Remove prepare_video_session/service --- iblrig/base_tasks.py | 11 ++- iblrig/path_helper.py | 2 +- iblrig/test/test_video.py | 58 +------------ iblrig/video.py | 174 +------------------------------------- pyproject.toml | 2 +- 5 files changed, 16 insertions(+), 231 deletions(-) diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 927e1fbd5..5d54826ad 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -130,7 +130,12 @@ def __init__( self.init_datetime = datetime.datetime.now() # loads in the settings: first load the files, then update with the input argument if provided - self._load_settings(file_hardware_settings=file_hardware_settings, hardware_settings=hardware_settings, file_iblrig_settings=file_iblrig_settings, iblrig_settings=iblrig_settings) + self._load_settings( + file_hardware_settings=file_hardware_settings, + hardware_settings=hardware_settings, + file_iblrig_settings=file_iblrig_settings, + iblrig_settings=iblrig_settings, + ) self.wizard = wizard # Load the tasks settings, from the task folder or override with the input argument @@ -166,7 +171,9 @@ def __init__( extractors=self.extractor_tasks, ) - def _load_settings(self, file_hardware_settings=None, hardware_settings=None, file_iblrig_settings=None, iblrig_settings=None, **_): + def _load_settings( + self, file_hardware_settings=None, hardware_settings=None, file_iblrig_settings=None, iblrig_settings=None, **_ + ): self.hardware_settings: HardwareSettings = load_pydantic_yaml(HardwareSettings, file_hardware_settings) if hardware_settings is not None: self.hardware_settings.update(hardware_settings) diff --git a/iblrig/path_helper.py b/iblrig/path_helper.py index 3b603b8d4..07e1cbe04 100644 --- a/iblrig/path_helper.py +++ b/iblrig/path_helper.py @@ -148,7 +148,7 @@ def get_local_and_remote_paths( if iblrig_settings is None and ((local_path is None) or (remote_path is None) or (lab is None)): iblrig_settings = load_pydantic_yaml(RigSettings) if isinstance(iblrig_settings, RigSettings): - iblrig_settings = iblrig_settings.model_dump() + iblrig_settings = iblrig_settings.model_dump() paths = Bunch({'local_data_folder': local_path, 'remote_data_folder': remote_path}) if paths.local_data_folder is None: diff --git a/iblrig/test/test_video.py b/iblrig/test/test_video.py index e829230ae..33603d63a 100644 --- a/iblrig/test/test_video.py +++ b/iblrig/test/test_video.py @@ -17,7 +17,7 @@ from iblrig import video # noqa from iblrig.test.base import BaseTestCases # noqa -from iblrig.path_helper import load_pydantic_yaml, HARDWARE_SETTINGS_YAML, RIG_SETTINGS_YAML # noqa +from iblrig.path_helper import load_pydantic_yaml # noqa from iblrig.pydantic_definitions import HardwareSettings # noqa @@ -40,60 +40,6 @@ def test_download_from_alyx_or_flir(self, mock_os_rename, mock_hashfile, mock_aw mock_os_rename.assert_called_once_with(Path('mocked_tmp_file'), expected_out_file) -class TestPrepareVideoSession(unittest.TestCase): - """Test for iblrig.video.prepare_video_session.""" - - def setUp(self): - self.tmp = tempfile.TemporaryDirectory() - self.subject = 'foobar' - self.addCleanup(self.tmp.cleanup) - (input_mock := patch('builtins.input')).start() - self.addCleanup(input_mock.stop) - - @patch('iblrig.video.EmptySession') - @patch('iblrig.video.HAS_PYSPIN', True) - @patch('iblrig.video.HAS_SPINNAKER', True) - @patch('iblrig.video.call_bonsai') - @patch('iblrig.video_pyspin.enable_camera_trigger') - def test_prepare_video_session(self, enable_camera_trigger, call_bonsai, session): - """Test iblrig.video.prepare_video_session function.""" - # Set up mock session folder - session_path = Path(self.tmp.name, self.subject, '2020-01-01', '001') - session().paths.SESSION_FOLDER = session_path - session_path.mkdir(parents=True) - # Set up remote path - remote_path = Path(self.tmp.name, 'remote', self.subject, '2020-01-01', '001') - session().paths.REMOTE_SUBJECT_FOLDER = remote_path - remote_path.mkdir(parents=True) - # Some test hardware settings - hws = load_pydantic_yaml(HardwareSettings, 'hardware_settings_template.yaml') - hws['device_cameras']['default']['right'] = hws['device_cameras']['default']['left'] - session().hardware_settings = hws - workflows = hws['device_cameras']['default']['BONSAI_WORKFLOW'] - - video.prepare_video_session(self.subject, 'default') - - # Validate calls - expected = [call(enable=False), call(enable=True), call(enable=False)] - enable_camera_trigger.assert_has_calls(expected) - raw_data_folder = session_path / 'raw_video_data' - expected_pars = { - 'LeftCameraIndex': 1, - 'RightCameraIndex': 1, - 'FileNameLeft': str(raw_data_folder / '_iblrig_leftCamera.raw.avi'), - 'FileNameLeftData': str(raw_data_folder / '_iblrig_leftCamera.frameData.bin'), - 'FileNameRight': str(raw_data_folder / '_iblrig_rightCamera.raw.avi'), - 'FileNameRightData': str(raw_data_folder / '_iblrig_rightCamera.frameData.bin'), - } - expected = [call(workflows.setup, ANY, debug=False), call(workflows.recording, expected_pars, wait=False, debug=False)] - call_bonsai.assert_has_calls(expected) - - # Test config validation - self.assertRaises(ValueError, video.prepare_video_session, self.subject, 'training') - session().hardware_settings = hws.model_construct() - self.assertRaises(ValueError, video.prepare_video_session, self.subject, 'training') - - class BaseCameraTest(BaseTestCases.CommonTestTask): """A base class for camera hardware test fixtures.""" @@ -217,7 +163,7 @@ def _end_bonsai_proc(): """Return args with added side effect of signalling Bonsai subprocess termination.""" addr = '192.168.0.5:99998' info_msg = ((net.base.ExpStatus.CONNECTED, {'subject_name': 'foo'}), addr, net.base.ExpMessage.EXPINFO) - init_msg = ({'exp_ref': f'{date.today()}_1_foo'}, addr, net.base.ExpMessage.EXPINIT) + init_msg = ([{'exp_ref': f'{date.today()}_1_foo'}], addr, net.base.ExpMessage.EXPINIT) start_msg = ((f'{date.today()}_1_foo', {}), addr, net.base.ExpMessage.EXPSTART) status_msg = (net.base.ExpStatus.RUNNING, addr, net.base.ExpMessage.EXPSTATUS) for call_number, msg in enumerate((info_msg, init_msg, start_msg, status_msg, status_msg)): diff --git a/iblrig/video.py b/iblrig/video.py index c7b047ab5..e462bde76 100644 --- a/iblrig/video.py +++ b/iblrig/video.py @@ -197,8 +197,6 @@ def prepare_video_session_cmd(): # Technically `prepare_video_service` should behave the same as `prepare_video_session` if the service_uri arg is # False but until fully tested, let's call the old function if service_uri is False: - # TODO Use CameraSession object and remove prepare_video_session and prepare_video_service - # prepare_video_session(args.subject_name, args.profile, debug=args.debug) session = CameraSession(subject=args.subject_name, config_name=args.profile, log_level=log_level) session.run() else: @@ -298,174 +296,6 @@ def validate_video(video_path, config): return ok -def prepare_video_session(subject_name: str, config_name: str, debug: bool = False): - """ - Setup and record video. - - Parameters - ---------- - subject_name : str - A subject name. - config_name : str - Camera configuration name, found in "device_cameras" map of hardware_settings.yaml. - debug : bool - Bonsai debug mode and verbose logging. - """ - assert HAS_SPINNAKER - assert HAS_PYSPIN - - # Initialize a session for paths and settings - session = EmptySession(subject=subject_name, interactive=False) - session_path = session.paths.SESSION_FOLDER - raw_data_folder = session_path.joinpath('raw_video_data') - - # Fetch camera configuration from hardware settings file - try: - config = session.hardware_settings.device_cameras[config_name] - except AttributeError as ex: - if hasattr(value_error := ValueError('"No camera config in hardware_settings.yaml file."'), 'add_note'): - value_error.add_note(str(HARDWARE_SETTINGS_YAML)) # py 3.11 - raise value_error from ex - except KeyError as ex: - raise ValueError(f'Config "{config_name}" not in "device_cameras" hardware settings.') from ex - workflows = config.pop('BONSAI_WORKFLOW') - cameras = [k for k in config if k != 'BONSAI_WORKFLOW'] - params = {f'{k.capitalize()}CameraIndex': config[k].INDEX for k in cameras} - raw_data_folder.mkdir(parents=True, exist_ok=True) - - # align cameras - if workflows.setup: - video_pyspin.enable_camera_trigger(enable=False) - call_bonsai(workflows.setup, params, debug=debug) - - # record video - filenamevideo = '_iblrig_{}Camera.raw.avi' - filenameframedata = '_iblrig_{}Camera.frameData.bin' - for k in map(str.capitalize, cameras): - params[f'FileName{k}'] = str(raw_data_folder / filenamevideo.format(k.lower())) - params[f'FileName{k}Data'] = str(raw_data_folder / filenameframedata.format(k.lower())) - video_pyspin.enable_camera_trigger(enable=True) - bonsai_process = call_bonsai(workflows.recording, params, wait=False, debug=debug) - input('PRESS ENTER TO START CAMERAS') - # Save the stub files locally and in the remote repo for future copy script to use - copier = VideoCopier(session_path=session_path, remote_subjects_folder=session.paths.REMOTE_SUBJECT_FOLDER) - copier.initialize_experiment(acquisition_description=copier.config2stub(config, raw_data_folder.name)) - - video_pyspin.enable_camera_trigger(enable=False) - log.info('To terminate video acquisition, please stop and close Bonsai workflow.') - bonsai_process.wait() - log.info('Video acquisition session finished.') - - # Check video files were saved and configured correctly - for video_file in (Path(v) for v in params.values() if isinstance(v, str) and v.endswith('.avi')): - validate_video(video_file, config[label_from_path(video_file)]) - - session_path.joinpath('transfer_me.flag').touch() - # remove empty-folders and parent-folders - if not any(raw_data_folder.iterdir()): - os.removedirs(raw_data_folder) - - -async def prepare_video_service(config_name: str, debug: bool = False, service_uri=None, subject_name=None): - """ - Setup and record video. - - Parameters - ---------- - config_name : str - Camera configuration name, found in "device_cameras" map of hardware_settings.yaml. - debug : bool - Bonsai debug mode and verbose logging. - service_uri : str - The service URI. - """ - assert HAS_SPINNAKER - assert HAS_PYSPIN - - com, _ = await get_server_communicator(service_uri, 'cameras') - - if not (com or subject_name): - raise ValueError('Please provide a subject name or service_uri.') - - # Initialize a session for paths and settings - session = EmptySession(subject=subject_name or '', interactive=False) - - # Fetch camera configuration from hardware settings file - try: - config = session.hardware_settings.device_cameras[config_name] - except AttributeError as ex: - if hasattr(value_error := ValueError('"No camera config in hardware_settings.yaml file."'), 'add_note'): - value_error.add_note(HARDWARE_SETTINGS_YAML) # py 3.11 - raise value_error from ex - except KeyError as ex: - raise ValueError(f'Config "{config_name}" not in "device_cameras" hardware settings.') from ex - workflows = config.pop('BONSAI_WORKFLOW') - cameras = [k for k in config if k != 'BONSAI_WORKFLOW'] - params = {f'{k.capitalize()}CameraIndex': config[k].INDEX for k in cameras} - - # align cameras - if workflows.setup: - video_pyspin.enable_camera_trigger(enable=False) - call_bonsai(workflows.setup, params, debug=debug) - - # Wait for initialization - if com: - # TODO Add exp info callback for main sync determination - data, addr = await com.on_event(net.base.ExpMessage.EXPINIT) - exp_ref = (data or {}).get('exp_ref') - assert exp_ref, 'No experiment reference found' - if isinstance(exp_ref, str): - exp_ref = ConversionMixin.ref2dict(exp_ref) - assert not subject_name or (subject_name == exp_ref['subject']) - session_path = session.paths.LOCAL_SUBJECT_FOLDER.joinpath( - exp_ref['subject'], str(exp_ref['date']), f'{exp_ref["sequence"]:03}' - ) - else: - session_path = session.paths.SESSION_FOLDER - - raw_data_folder = session_path.joinpath('raw_video_data') - raw_data_folder.mkdir(parents=True, exist_ok=True) - - # initialize video - filenamevideo = '_iblrig_{}Camera.raw.avi' - filenameframedata = '_iblrig_{}Camera.frameData.bin' - for k in map(str.capitalize, cameras): - params[f'FileName{k}'] = str(raw_data_folder / filenamevideo.format(k.lower())) - params[f'FileName{k}Data'] = str(raw_data_folder / filenameframedata.format(k.lower())) - video_pyspin.enable_camera_trigger(enable=True) - bonsai_process = call_bonsai(workflows.recording, params, wait=False, debug=debug) - - copier = VideoCopier(session_path=session_path, remote_subjects_folder=session.paths.REMOTE_SUBJECT_FOLDER) - description = copier.config2stub(config, raw_data_folder.name) - if com: - await com.init({'experiment_description': description}, addr=addr) - log.info('initialized.') - # Wait for task to begin - data, addr = await com.on_event(net.base.ExpMessage.EXPSTART) - else: - input('PRESS ENTER TO START CAMERAS') - - # Save the stub files locally and in the remote repo for future copy script to use - copier.initialize_experiment(acquisition_description=copier.config2stub(config, raw_data_folder.name)) - - video_pyspin.enable_camera_trigger(enable=False) - if com: - await com.start(ConversionMixin.dict2ref(exp_ref), addr=addr) # Let behaviour PC know acquisition has started - log.info('To terminate video acquisition, please stop and close Bonsai workflow.') - bonsai_process.wait() - log.info('Video acquisition session finished.') - - # Check video files were saved and configured correctly - for video_file in (Path(v) for v in params.values() if isinstance(v, str) and v.endswith('.avi')): - validate_video(video_file, config[label_from_path(video_file)]) - - session_path.joinpath('transfer_me.flag').touch() - # remove empty-folders and parent-folders - if not any(raw_data_folder.iterdir()): - os.removedirs(raw_data_folder) - com.close() - - class CameraSession(EmptySession): def __init__(self, subject=None, config_name='default', **kwargs): """ @@ -792,7 +622,9 @@ async def on_init(self, data, addr): """Process init command from remote rig.""" self.logger.info('INIT message received') data = data[0] if any(data) else {} - assert (exp_ref := data.get('exp_ref')), 'No experiment reference found' # FIXME graceful error (stop thread somehow so there's no hanging) + assert ( + exp_ref := data.get('exp_ref') + ), 'No experiment reference found' # FIXME graceful error (stop thread somehow so there's no hanging) if isinstance(exp_ref, str): exp_ref = self.one.ref2dict(exp_ref) # NB: Only the first match case for which predicate is true will be run so we can update the status dynamically diff --git a/pyproject.toml b/pyproject.toml index be21f5e0e..e2ae58c07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ files = [ "iblrig/[!test]**/*.py", "iblrig_tasks/**/*.py" ] ignore_missing_imports = true [tool.pytest.ini_options] -addopts = "-ra --showlocals --cov --cov-report=html --cov-report=xml --tb=short" +addopts = "-ra --showlocals --tb=short" minversion = "6.0" testpaths = [ "iblrig/test" ] python_files = [ "test_*.py" ] From dd483f8eb118ec1b1d6882b7bf8171be50e58b68 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 12 Dec 2024 12:33:17 +0200 Subject: [PATCH 09/11] Log when network cleanup called --- .gitignore | 1 + iblrig/base_tasks.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 05408a877..e9615b710 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ __pycache__/ \.sonarlint/ \.pytest* .pytest* +*.code-workspace *.pyc docs/_build scratch/ diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 5d54826ad..3f497e303 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -1437,9 +1437,12 @@ def stop_mixin_network(self): def cleanup_mixin_network(self): """Clean up services.""" + log.info('Cleaning up network mixin') self.remote_rigs.close() if self.remote_rigs.is_connected: log.warning('Failed to properly clean up network mixin') + else: + log.info('Cleaned up network mixin') class SpontaneousSession(BaseSession): From be117b7180408ac871a7efc974033d6c9a0cf2cd Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Tue, 21 Jan 2025 15:21:01 +0200 Subject: [PATCH 10/11] Move remote argument from ChoiceWorld.extra_parser to NetworkSession.extra_parser Resolves https://github.com/int-brain-lab/project_extraction/issues/28 --- iblrig/base_choice_world.py | 9 --------- iblrig/base_tasks.py | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/iblrig/base_choice_world.py b/iblrig/base_choice_world.py index 5ecb9e89a..369241642 100644 --- a/iblrig/base_choice_world.py +++ b/iblrig/base_choice_world.py @@ -152,15 +152,6 @@ def extra_parser(): required=False, help='initial delay before starting the first trial (default: 0 min)', ) - parser.add_argument( - '--remote', - dest='remote_rigs', - type=str, - required=False, - action='append', - nargs='+', - help='specify one of the remote rigs to interact with over the network', - ) return parser def start_hardware(self): diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index 3f497e303..cd10fb36e 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -1319,6 +1319,27 @@ def _init_paths(self, append: bool = False): self.session_info.SESSION_NUMBER = int(paths.SESSION_FOLDER.name) return paths + @staticmethod + def extra_parser(): + """ + Parse network arguments. + + Namely adds the remote argument to the parser. + + :return: argparse.parser() + """ + parser = super().extra_parser() + parser.add_argument( + '--remote', + dest='remote_rigs', + type=str, + required=False, + action='append', + nargs='+', + help='specify one of the remote rigs to interact with over the network', + ) + return parser + def run(self): """Run session and report exceptions to remote services.""" self.start_mixin_network() From 481ca3d843d1864eba3cdcddfa95c71317d34be7 Mon Sep 17 00:00:00 2001 From: Florian Rau Date: Tue, 21 Jan 2025 15:06:50 +0000 Subject: [PATCH 11/11] fix argparse --- iblrig/base_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iblrig/base_tasks.py b/iblrig/base_tasks.py index cd10fb36e..49e1e7226 100644 --- a/iblrig/base_tasks.py +++ b/iblrig/base_tasks.py @@ -1328,7 +1328,7 @@ def extra_parser(): :return: argparse.parser() """ - parser = super().extra_parser() + parser = super(NetworkSession, NetworkSession).extra_parser() parser.add_argument( '--remote', dest='remote_rigs',