Skip to content

Commit

Permalink
Merge pull request #769 from int-brain-lab/iblrigv8dev
Browse files Browse the repository at this point in the history
8.27.3
  • Loading branch information
bimac authored Feb 19, 2025
2 parents b6bd70f + 1b5c220 commit 7b986ae
Show file tree
Hide file tree
Showing 15 changed files with 712 additions and 543 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

8.27.3
------
* changed: reset camera(s) prior to starting task when inconsistencies have been detected
* changed: include stock subjects if user is stock manager
* changed: skip validation of Ambient Module if device_bpod.USE_AMBIENT_MODULE is false
* added: `remove_bonsai_layouts` command for troubleshooting BONSAI GUIs not appearing

8.27.2
------
* fixed: error when using task arguments of type bool
Expand Down
9 changes: 9 additions & 0 deletions docs/source/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ Camera Issues
* If you use a USB 3.1 Host Controller Card check if it requires additional powering through a SATA or Molex cable.
FLIR offers `a few models <https://www.flir.com/products/usb-3.1-host-controller-card>`_ that should work fine.

* If the BONSAI panels for the video live view do not show up there might be an issue with the
`BONSAI layout settings <https://bonsai-rx.org/docs/articles/editor.html#visualizer-layout-settings>`_.
To reset these layout settings, run the following in PowerShell:

.. code::
C:\iblrigv8\venv\scripts\Activate.ps1
remove_bonsai_layouts
Frame2TTL
=========
Expand Down
2 changes: 1 addition & 1 deletion docs/source/usage_video.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Below shows how to start the cameras for the subject 'example' with configuratio
cd C:\iblrigv8\
venv\scripts\Activate.ps1
start_video_session example default
start_video_session --subject_name example --profile default
Copy command
------------
Expand Down
2 changes: 1 addition & 1 deletion iblrig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
# 5) git tag the release in accordance to the version number below (after merge!)
# >>> git tag 8.15.6
# >>> git push origin --tags
__version__ = '8.27.2'
__version__ = '8.27.3'
7 changes: 5 additions & 2 deletions iblrig/base_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,11 +806,14 @@ def start_mixin_bonsai_cameras(self):
if (workflow_file := self._camera_mixin_bonsai_get_workflow_file(configuration, 'setup')) is None:
return

# test acquisition and reset cameras if needed
# enable trigger of cameras (so Bonsai can disable it again ... sigh)
if PYSPIN_AVAILABLE:
from iblrig.video_pyspin import enable_camera_trigger
from iblrig import video_pyspin

enable_camera_trigger(True)
if not video_pyspin.acquisition_ok():
video_pyspin.reset_all_cameras()
video_pyspin.enable_camera_trigger(True)

call_bonsai(workflow_file, wait=True) # TODO Parameterize using configuration cameras
log.info('Bonsai cameras setup module loaded: OK')
Expand Down
19 changes: 19 additions & 0 deletions iblrig/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,22 @@ def flush():
bpod = Bpod(hardware_settings['device_bpod']['COM_BPOD'])
bpod.flush()
bpod.close()


def remove_bonsai_layouts():
"""Delete all BONSAI .layout files - if they are backed up with a .layout_template file."""
from iblrig.constants import BASE_PATH

layout_files = [x for x in BASE_PATH.glob('**/*.bonsai.layout') if x.with_suffix('.layout_template').exists()]
if len(layout_files) == 0:
print('No layout files found.')
return
print('The following files will be deleted:')
for f in layout_files:
print(f'- {f.name}')
if input('\nContinue? [Y/n] ').lower() in ('y', ''):
for f in layout_files:
f.unlink()
print(f'{len(layout_files)} file{"s" if len(layout_files) > 1 else ""} deleted.')
else:
print('No files deleted.')
11 changes: 7 additions & 4 deletions iblrig/gui/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,13 @@ def login(
)
QtWidgets.QMessageBox().critical(None, 'Error', f'{message}\n\n{solution}')

# get subjects from Alyx: this is the set of subjects that are alive and not stock in the lab defined in settings
rest_subjects = self.alyx.rest(
'subjects', 'list', alive=True, stock=False, lab=self.iblrig_settings['ALYX_LAB'], no_cache=True
)
# get subjects from Alyx: this is the set of subjects that are alive and in the lab defined in settings
# stock subjects are excluded, unless the user is stock manager
kwargs = {'alive': True, 'lab': self.iblrig_settings['ALYX_LAB'], 'no_cache': True}
is_stock_manager = any(self.alyx.rest('subjects', 'list', responsible_user=self.user, stock=True, limit=1, **kwargs))
if not is_stock_manager:
kwargs['stock'] = False
rest_subjects = self.alyx.rest('subjects', 'list', **kwargs)
self.all_subjects.remove(self.test_subject_name)
self.all_subjects = [self.test_subject_name] + sorted(set(self.all_subjects + [s['nickname'] for s in rest_subjects]))

Expand Down
5 changes: 5 additions & 0 deletions iblrig/hardware_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ class ValidatorAmbientModule(Validator):
_name = 'Bpod Ambient Module'

def _run(self):
# skip if ambient module is not being used
if not self.hardware_settings.device_bpod.USE_AMBIENT_MODULE:
yield Result(Status.SKIP, 'Ambient module is not being used - skipping validation')
return False

# yield Bpod's connection status
bpod = yield from self._get_bpod()
if bpod is None:
Expand Down
1 change: 1 addition & 0 deletions iblrig/pydantic_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class HardwareSettingsBpod(BunchModel):
SOUND_BOARD_BPOD_PORT: Literal['Serial1', 'Serial2', 'Serial3', 'Serial4', 'Serial5', None] = None
ROTARY_ENCODER_BPOD_PORT: Literal['Serial1', 'Serial2', 'Serial3', 'Serial4', 'Serial5', None] = None
DISABLE_BEHAVIOR_INPUT_PORTS: list[BehaviourInputPort] = [2, 3, 4]
USE_AMBIENT_MODULE: bool = True


class HardwareSettingsFrame2TTL(BunchModel):
Expand Down
26 changes: 0 additions & 26 deletions iblrig/test/test_commands.py

This file was deleted.

10 changes: 10 additions & 0 deletions iblrig/test/test_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import unittest
from collections.abc import Callable
from importlib.metadata import entry_points


class TestEntryPoints(unittest.TestCase):
def test_entry_points(self) -> None:
for ep in [ep for ep in entry_points(group='console_scripts') if ep.value.startswith('iblrig.')]:
loaded_ep = ep.load() # this throws a ModuleNotFound error if the entry-point is invalid
assert isinstance(loaded_ep, Callable)
135 changes: 130 additions & 5 deletions iblrig/video_pyspin.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,159 @@
import logging
import time

import PySpin

log = logging.getLogger(__name__)


class Cameras:
"""A class to manage camera instances using the PySpin library.
This class provides a context manager for initializing and deinitializing
cameras. It ensures that cameras are properly initialized when entering
the context and deinitialized when exiting.
"""

_instance = None

def __init__(self, init_cameras: bool = True):
"""Initializes the Cameras instance.
Parameters
----------
init_cameras : bool, optional
If True, initializes the cameras upon creation of the instance (default is True).
"""
self._instance = PySpin.System.GetInstance()
self._cameras = self._instance.GetCameras()
if not init_cameras:
return
for camera in self._cameras:
camera.Init()
self._init_cameras = init_cameras
if init_cameras:
for camera in self._cameras:
camera.Init()

def __enter__(self) -> PySpin.CameraList:
"""Enters the runtime context related to this object.
Returns
-------
PySpin.CameraList
The list of initialized cameras.
"""
return self._cameras

def __exit__(self, *_):
"""Exits the runtime context related to this object.
Deinitializes the cameras if they were initialized and releases the system instance.
"""
if self._init_cameras:
for camera in self._cameras:
camera.DeInit()
del camera # Clean up the camera reference
self._cameras.Clear()
self._instance.ReleaseInstance()

@property
def instance(self):
"""Gets the singleton instance of the PySpin system.
Returns
-------
PySpin.System
The singleton instance of the PySpin system.
"""
return self._instance


def acquisition_ok() -> bool:
"""Test image acquisition for all available cameras.
This function attempts to acquire an image from each camera and checks if the acquisition
was successful. It logs the results of the acquisition test for each camera.
Returns
-------
bool
True if all cameras successfully acquired an image, False otherwise.
"""
success = True
with Cameras() as cameras:
for camera in cameras:
log.debug(f'Testing image acquisition with camera #{camera.DeviceID()}')
camera.BeginAcquisition()
try:
image = camera.GetNextImage(1000)
if image.IsValid() and image.GetImageStatus() == PySpin.SPINNAKER_IMAGE_STATUS_NO_ERROR:
log.info(f'Acquisition test for camera #{camera.DeviceID()} was successful.')
else:
log.error(f'Inconsistency detected during acquisition test for camera #{camera.DeviceID()}.')
success = False
except PySpin.SpinnakerException as e:
log.error(f'Acquisition test for camera #{camera.DeviceID()} failed with an exception: {e.message}')
success = False
else:
if image.IsValid():
image.Release()
finally:
camera.EndAcquisition()
del camera
return success


def reset_all_cameras():
"""Reset all available cameras and wait for them to come back online.
This function initializes each camera, attempts to reset it, and then deinitializes it.
After resetting, it waits for all cameras to come back online, logging the status of each camera.
"""
with Cameras(init_cameras=False) as cameras:
if len(cameras) == 0:
return

# Iterate through each camera and reset
for camera in cameras:
camera.Init()
try:
camera.DeviceReset()
except PySpin.SpinnakerException as e:
log.error(f'Error resetting camera #{camera.DeviceID()}: {e}')
else:
log.info(f'Resetting camera #{camera.DeviceID.ToString()} ...')
finally:
camera.DeInit()

# Wait for all cameras to come back online
log.info(f'Waiting for {"camera" if len(cameras) == 1 else "cameras"} to come back online (~10 s) ...')
all_cameras_online = False
while not all_cameras_online:
all_cameras_online = True
for camera in cameras:
try:
camera.Init()
except PySpin.SpinnakerException:
all_cameras_online = False
else:
log.info(f'Camera #{camera.DeviceID()} is back online.')
camera.DeInit()
if not all_cameras_online:
time.sleep(0.2)
del camera


def enable_camera_trigger(enable: bool, camera: PySpin.CameraPtr | None = None):
"""Enable or disable the trigger for a specified camera or all cameras.
This function allows you to enable or disable the trigger mode for a given camera.
If no camera is specified, it will enable or disable the trigger mode for all available cameras.
Parameters
----------
enable : bool
A flag indicating whether to enable (True) or disable (False) the camera trigger.
camera : PySpin.CameraPtr | None, optional
A pointer to a specific camera instance. If None, the function will apply the trigger setting
to all cameras managed by the Cameras context manager (default is None).
"""
if camera is None:
with Cameras() as cameras:
for cam in cameras:
Expand All @@ -39,4 +164,4 @@ def enable_camera_trigger(enable: bool, camera: PySpin.CameraPtr | None = None):
node_trigger_mode = PySpin.CEnumerationPtr(node_map.GetNode('TriggerMode'))
node_trigger_mode_value = node_trigger_mode.GetEntryByName('On' if enable else 'Off').GetValue()
node_trigger_mode.SetIntValue(node_trigger_mode_value)
log.debug(('Enabled' if enable else 'Disabled') + f' trigger for camera #{camera.DeviceID.ToString()}.')
log.debug(('Enabled' if enable else 'Disabled') + f' trigger for camera #{camera.DeviceID()}.')
Loading

0 comments on commit 7b986ae

Please sign in to comment.