From 41473ea0890e00d8e5d981b56ffc4b51101ce9b1 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:02:30 +0200 Subject: [PATCH 1/8] Update the base part (workflow, setup) to the newest template state. --- .github/workflows/python-publish.yml | 14 +++-- plugin_info.toml | 2 +- setup.py | 71 +------------------------- tests/test_plugin_package_structure.py | 28 ++++++---- 4 files changed, 30 insertions(+), 85 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index cc1ffb5..6dd38ac 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,15 +13,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine toml + pip install setuptools wheel twine toml "pymodaq>=4.1.0" pyqt5 + + - name: create local pymodaq folder and setting permissions + run: | + sudo mkdir /etc/.pymodaq + sudo chmod uo+rw /etc/.pymodaq + - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} diff --git a/plugin_info.toml b/plugin_info.toml index 70006ff..35e86b7 100644 --- a/plugin_info.toml +++ b/plugin_info.toml @@ -11,7 +11,7 @@ license = 'MIT' [plugin-install] #packages required for your plugin: packages-required = [ - 'pymodaq>=4.0.0', + 'pymodaq>=4.1.0', 'numpy', # for Basler camera 'pypylon', ] diff --git a/setup.py b/setup.py index af936e8..c85f63a 100644 --- a/setup.py +++ b/setup.py @@ -1,71 +1,4 @@ - +from pymodaq.resources.setup_plugin import setup from pathlib import Path -try: - from pymodaq.resources.setup_plugin import setup as _setup -except ModuleNotFoundError: - fallback = True - from setuptools import setup as _setup, find_packages - import toml -else: - fallback = False - - -def setup(): - if not fallback: - return _setup() - - config = toml.load('./plugin_info.toml') - SHORT_PLUGIN_NAME = config['plugin-info']['SHORT_PLUGIN_NAME'] - PLUGIN_NAME = f"pymodaq_plugins_{SHORT_PLUGIN_NAME}" - - if not SHORT_PLUGIN_NAME.isidentifier(): - raise ValueError("'SHORT_PLUGIN_NAME = %s' is not a valid python identifier." % SHORT_PLUGIN_NAME) - - version_file = Path(__file__).parent.joinpath(f'src/{PLUGIN_NAME}/resources/VERSION') # new location of the version file - if not version_file.is_file(): - version_file = Path(__file__).parent.joinpath(f'src/{PLUGIN_NAME}/VERSION') - - with open(str(version_file), 'r') as fvers: - version = fvers.read().strip() - - - with open('README.rst') as fd: - long_description = fd.read() - - setupOpts = dict( - name=PLUGIN_NAME, - description=config['plugin-info']['description'], - long_description=long_description, - license=config['plugin-info']['license'], - url=config['plugin-info']['package-url'], - author=config['plugin-info']['author'], - author_email=config['plugin-info']['author-email'], - classifiers=[ - "Programming Language :: Python :: 3", - "Development Status :: 5 - Production/Stable", - "Environment :: Other Environment", - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering :: Human Machine Interfaces", - "Topic :: Scientific/Engineering :: Visualization", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: User Interfaces", - ], ) - - - return _setup( - version=version, - packages=find_packages(where='./src'), - package_dir={'': 'src'}, - include_package_data=True, - entry_points={'pymodaq.plugins': f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}', - 'pymodaq.pid_models': f"{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}", - 'pymodaq.extensions': f"{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}"}, - install_requires=['toml', ]+config['plugin-install']['packages-required'], - **setupOpts - ) - - -setup() +setup(Path(__file__).parent) diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index be4e4ee..1c44804 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -21,24 +21,30 @@ def get_package_name(): package_name = here.parent.stem return package_name - def get_move_plugins(): pkg_name = get_package_name() - move_mod = importlib.import_module(f'{pkg_name}.daq_move_plugins') - - plugin_list = [mod for mod in [mod[1] for mod in - pkgutil.iter_modules([str(move_mod.path.parent)])] - if 'daq_move_' in mod] + try: + move_mod = importlib.import_module(f'{pkg_name}.daq_move_plugins') + plugin_list = [mod for mod in [mod[1] for mod in + pkgutil.iter_modules([str(move_mod.path.parent)])] + if 'daq_move_' in mod] + except ModuleNotFoundError: + plugin_list = [] + move_mod = None return plugin_list, move_mod def get_viewer_plugins(dim='0D'): pkg_name = get_package_name() - viewer_mod = importlib.import_module(f'{pkg_name}.daq_viewer_plugins.plugins_{dim}') - - plugin_list = [mod for mod in [mod[1] for mod in - pkgutil.iter_modules([str(viewer_mod.path.parent)])] - if f'daq_{dim}viewer_' in mod] + try: + viewer_mod = importlib.import_module(f'{pkg_name}.daq_viewer_plugins.plugins_{dim}') + + plugin_list = [mod for mod in [mod[1] for mod in + pkgutil.iter_modules([str(viewer_mod.path.parent)])] + if f'daq_{dim}viewer_' in mod] + except ModuleNotFoundError: + plugin_list = [] + viewer_mod = None return plugin_list, viewer_mod From bd467b5b65a1c8557ab16a39a88c9407830a99da Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+bmoneke@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:39:15 +0200 Subject: [PATCH 2/8] Update GenericPylalib file and adjust basler to give pixel size. --- .../daq_2Dviewer_GenericPylablibCamera.py | 48 ++++--- src/pymodaq_plugins_basler/hardware/basler.py | 120 +++++++++++++----- 2 files changed, 118 insertions(+), 50 deletions(-) diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py index 9d6b73f..d60e2c1 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py +++ b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py @@ -25,16 +25,16 @@ class DAQ_2DViewer_GenericPylablibCamera(DAQ_Viewer_base): params = comon_parameters + [ {'title': 'Camera:', 'name': 'camera_list', 'type': 'list', 'limits': []}, - {'title': 'Camera model:', 'name': 'camera_info', 'type': 'str', 'value': '', 'readonly': True}, - {'title': 'Update ROI', 'name': 'update_roi', 'type': 'bool_push', 'value': False}, - {'title': 'Clear ROI+Bin', 'name': 'clear_roi', 'type': 'bool_push', 'value': False}, - {'title': 'Binning', 'name': 'binning', 'type': 'list', 'limits': [1, 2]}, - {'title': 'Image width', 'name': 'hdet', 'type': 'int', 'value': 1, 'readonly': True}, - {'title': 'Image height', 'name': 'vdet', 'type': 'int', 'value': 1, 'readonly': True}, + {'title': 'Camera model:', 'name': 'camera_info', 'type': 'str', 'value': '', 'readonly': True, 'default': ''}, + {'title': 'Update ROI', 'name': 'update_roi', 'type': 'bool_push', 'value': False, 'default': False}, + {'title': 'Clear ROI+Bin', 'name': 'clear_roi', 'type': 'bool_push', 'value': False, 'default': False}, + {'title': 'Binning', 'name': 'binning', 'type': 'list', 'limits': [1, 2], 'default': 1}, + {'title': 'Image width', 'name': 'hdet', 'type': 'int', 'value': 1, 'readonly': True, 'default': 1}, + {'title': 'Image height', 'name': 'vdet', 'type': 'int', 'value': 1, 'readonly': True, 'default': 1}, {'title': 'Timing', 'name': 'timing_opts', 'type': 'group', 'children': - [{'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 1}, - {'title': 'Compute FPS', 'name': 'fps_on', 'type': 'bool', 'value': True}, - {'title': 'FPS', 'name': 'fps', 'type': 'float', 'value': 0.0, 'readonly': True}] + [{'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 1, 'default': 1}, + {'title': 'Compute FPS', 'name': 'fps_on', 'type': 'bool', 'value': True, 'default': True}, + {'title': 'FPS', 'name': 'fps', 'type': 'float', 'value': 0.0, 'readonly': True, 'default': 0.0}] } ] callback_signal = QtCore.Signal() @@ -46,7 +46,7 @@ def init_controller(self): def ini_attributes(self): self.controller: None - + self.pixel_width = None # pixel size in microns self.x_axis = None self.y_axis = None self.last_tick = 0.0 # time counter used to compute FPS @@ -164,6 +164,10 @@ def ini_detector(self, controller=None): self.callback_thread.callback = callback self.callback_thread.start() + # Check if pixel width is available + if 'PixelWidth' in self.controller.get_all_attributes(): + self.pixel_width = self.controller.get_attribute_value('PixelWidth') + self._prepare_view() info = "Initialized camera" @@ -173,14 +177,11 @@ def ini_detector(self, controller=None): def _prepare_view(self): """Preparing a data viewer by emitting temporary data. Typically, needs to be called whenever the ROIs are changed""" - # wx = self.settings.child('rois', 'width').value() - # wy = self.settings.child('rois', 'height').value() - # bx = self.settings.child('rois', 'x_binning').value() - # by = self.settings.child('rois', 'y_binning').value() - # - # sizex = wx // bx - # sizey = wy // by - (hstart, hend, vstart, vend, *_) = self.controller.get_roi() + (hstart, hend, vstart, vend, *binning) = self.controller.get_roi() + try: + xbin, ybin = binning + except ValueError: # some Pylablib `get_roi` do return just four values instead of six + xbin = ybin = 1 height = hend - hstart width = vend - vstart @@ -188,11 +189,18 @@ def _prepare_view(self): self.settings.child('vdet').setValue(height) mock_data = np.zeros((width, height)) - self.x_axis = Axis(data=np.linspace(0, width, width, endpoint=False), label='Pixels', index=0) + if self.pixel_width: # if pixel_width is defined + scaling = self.pixel_width + unit = 'um' + else: + scaling = 1 + unit = 'Pxls' + + self.x_axis = Axis(offset = vstart * scaling, scaling=scaling * xbin, size=width // xbin, label="X", units=unit, index=0) if width != 1 and height != 1: data_shape = 'Data2D' - self.y_axis = Axis(data=np.linspace(0, height, height, endpoint=False), label='Pixels', index=1) + self.y_axis = Axis(offset= hstart * scaling, scaling=scaling * ybin, size=height // ybin, label='Y', units=unit, index=1) self.axes = [self.x_axis, self.y_axis] else: diff --git a/src/pymodaq_plugins_basler/hardware/basler.py b/src/pymodaq_plugins_basler/hardware/basler.py index 014a891..b2dd980 100644 --- a/src/pymodaq_plugins_basler/hardware/basler.py +++ b/src/pymodaq_plugins_basler/hardware/basler.py @@ -1,7 +1,7 @@ - import logging from typing import Any, Callable, List, Optional, Tuple, Union +from numpy.typing import NDArray from pypylon import pylon from qtpy import QtCore @@ -12,6 +12,14 @@ log.addHandler(logging.NullHandler()) +pixel_lengths: dict[str, float] = { + # camera model name: pixel length in µm + "daA1280-54um": 3.75, + "daA2500-14um": 2.2, + "daA3840-45um": 2, +} + + class DartCamera: """Control a Basler Dart camera in the style of pylablib. @@ -20,26 +28,30 @@ class DartCamera: :param name: Full name of the device. :param callback: Callback method for each grabbed image """ + tlFactory: pylon.TlFactory camera: pylon.InstantCamera - def __init__(self, name: str, callback: Optional[Callable] = None, - **kwargs): + def __init__(self, name: str, callback: Optional[Callable] = None, **kwargs): super().__init__(**kwargs) # create camera object self.tlFactory = pylon.TlFactory.GetInstance() self.camera = pylon.InstantCamera() # register configuration event handler self.configurationEventHandler = ConfigurationHandler() - self.camera.RegisterConfiguration(self.configurationEventHandler, - pylon.RegistrationMode_ReplaceAll, - pylon.Cleanup_None) + self.camera.RegisterConfiguration( + self.configurationEventHandler, + pylon.RegistrationMode_ReplaceAll, + pylon.Cleanup_None, + ) # configure camera events self.imageEventHandler = ImageEventHandler() - self.camera.RegisterImageEventHandler(self.imageEventHandler, - pylon.RegistrationMode_Append, - pylon.Cleanup_None) + self.camera.RegisterImageEventHandler( + self.imageEventHandler, pylon.RegistrationMode_Append, pylon.Cleanup_None + ) + self._pixel_length: Optional[float] = None + self.attributes = {} self.open(name=name) if callback is not None: self.set_callback(callback=callback) @@ -48,8 +60,11 @@ def open(self, name: str) -> None: device = self.tlFactory.CreateDevice(name) self.camera.Attach(device) self.camera.Open() + self.attributes["PixelWidth"] = self.pixel_length - def set_callback(self, callback: Callable, replace_all: bool = True) -> None: + def set_callback( + self, callback: Callable[[NDArray], None], replace_all: bool = True + ) -> None: """Setup a callback method for continuous acquisition. :param callback: Method to be used in continuous mode. It should accept an array as input. @@ -76,9 +91,17 @@ def get_device_info(self) -> List[Any]: props)``. """ devInfo: pylon.DeviceInfo = self.camera.GetDeviceInfo() - return [devInfo.GetFullName(), devInfo.GetModelName(), devInfo.GetSerialNumber(), - devInfo.GetDeviceClass(), devInfo.GetDeviceVersion(), devInfo.GetVendorName(), - devInfo.GetFriendlyName(), devInfo.GetUserDefinedName(), None] + return [ + devInfo.GetFullName(), + devInfo.GetModelName(), + devInfo.GetSerialNumber(), + devInfo.GetDeviceClass(), + devInfo.GetDeviceVersion(), + devInfo.GetVendorName(), + devInfo.GetFriendlyName(), + devInfo.GetUserDefinedName(), + None, + ] def get_exposure(self) -> float: """Get the exposure time in s.""" @@ -98,7 +121,9 @@ def get_roi(self) -> Tuple[float, float, float, float, int, int]: ybin = self.camera.BinningVertical.GetValue() return x0, x0 + width, y0, y0 + height, xbin, ybin - def set_roi(self, hstart: int, hend: int, vstart: int, vend: int, hbin: int, vbin: int) -> None: + def set_roi( + self, hstart: int, hend: int, vstart: int, vend: int, hbin: int, vbin: int + ) -> None: camera = self.camera m_width, m_height = self.get_detector_size() inc = camera.Width.Inc # minimum step size @@ -118,7 +143,9 @@ def get_detector_size(self) -> Tuple[int, int]: """Return width and height of detector in pixels.""" return self.camera.SensorWidth.GetValue(), self.camera.SensorHeight.GetValue() - def wait_for_frame(self, since="lastread", nframes=1, timeout=20., error_on_stopped=False): + def wait_for_frame( + self, since="lastread", nframes=1, timeout=20.0, error_on_stopped=False + ): """ Wait for one or several new camera frames. @@ -137,6 +164,13 @@ def wait_for_frame(self, since="lastread", nframes=1, timeout=20., error_on_stop """ raise NotImplementedError("Not implemented") + def get_all_attributes(self): + self.attributes + + def get_attribute_value(self, name, error_on_missing=True): + """Get the camera attribute with the given name""" + return self.attributes[name] + def clear_acquisition(self): """Stop acquisition""" pass # TODO @@ -154,10 +188,12 @@ def read_newest_image(self): def close(self) -> None: self.camera.Close() self.camera.DetachDevice() + self._pixel_length = None + self.attributes.pop("PixelWidth", None) # drop it # additional methods, for use in the code - def get_one(self, timeout_ms: int = 1000): - """Get one image and return the (numpy) array of it.""" + def get_single_result(self, timeout_ms: int = 1000) -> pylon.GrabResult: + """Get one image and return the pylon `GrabResult`.""" args = [] if timeout_ms is not None: args.append(timeout_ms) @@ -166,27 +202,45 @@ def get_one(self, timeout_ms: int = 1000): else: result = self.camera.GrabOne() if result.GrabSucceeded(): - return result.GetArray() + return result else: raise TimeoutError("Grabbing exceeded timeout") - def start_grabbing(self, max_frame_rate=1000) -> None: + def get_one(self, timeout_ms: int = 1000): + """Get one image and return the (numpy) array of it.""" + result = self.get_single_result(timeout_ms=1000) + result.GetArray() + + def start_grabbing(self, max_frame_rate: int = 1000) -> None: """Start continuously to grab data. Whenever a grab succeeded, the callback defined in :meth:`set_callback` is called. """ self.camera.AcquisitionFrameRate.SetValue(max_frame_rate) self.camera.StartGrabbing( - pylon.GrabStrategy_LatestImageOnly, - pylon.GrabLoop_ProvidedByInstantCamera + pylon.GrabStrategy_LatestImageOnly, pylon.GrabLoop_ProvidedByInstantCamera ) def stop_grabbing(self) -> None: self.camera.StopGrabbing() + @property + def pixel_length(self) -> float: + """Get the pixel length of the camera in µm. + + :raises: KeyError if the pixel length of the specific model is not known + """ + if self._pixel_length is None: + model = self.camera.GetDeviceInfo().GetModelName() + try: + self._pixel_length = pixel_lengths[model] + except KeyError: + raise KeyError(f"No pixel length known for camera model '{model}'.") + return self._pixel_length + class ConfigurationHandler(pylon.ConfigurationEventHandler): - """Handles the configuration events.""" + """Handle the configuration events.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -194,21 +248,22 @@ def __init__(self, **kwargs): class ConfigurationHandlerSignals(QtCore.QObject): """Signals for the CameraEventHandler.""" + cameraRemoved = QtCore.pyqtSignal(object) def OnOpened(self, camera: pylon.InstantCamera) -> None: """Standard configuration after being opened.""" - camera.PixelFormat.SetValue('Mono12') - camera.GainAuto.SetValue('Off') - camera.ExposureAuto.SetValue('Off') + camera.PixelFormat.SetValue("Mono12") + camera.GainAuto.SetValue("Off") + camera.ExposureAuto.SetValue("Off") def OnCameraDeviceRemoved(self, camera: pylon.InstantCamera) -> None: - """Emit a signal, that the camera is removed.""" + """Emit a signal that the camera is removed.""" self.signals.cameraRemoved.emit(camera) class ImageEventHandler(pylon.ImageEventHandler): - """Handles the events and translates them so signals/slots.""" + """Handle the events and translates them to signals/slots.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -216,10 +271,11 @@ def __init__(self, **kwargs): class ImageEventHandlerSignals(QtCore.QObject): """Signals for the ImageEventHandler.""" + imageGrabbed = QtCore.pyqtSignal(object) def OnImageSkipped(self, camera: pylon.InstantCamera, countOfSkippedImages: int) -> None: - """What to do if images are skipped.""" + """Handle a skipped image.""" log.warning(f"{countOfSkippedImages} images have been skipped.") def OnImageGrabbed(self, camera: pylon.InstantCamera, grabResult: pylon.GrabResult) -> None: @@ -227,8 +283,12 @@ def OnImageGrabbed(self, camera: pylon.InstantCamera, grabResult: pylon.GrabResu if grabResult.GrabSucceeded(): self.signals.imageGrabbed.emit(grabResult.GetArray()) else: - log.warning((f"Grab failed with code {grabResult.GetErrorCode()}, " - f"{grabResult.GetErrorDescription()}.")) + log.warning( + ( + f"Grab failed with code {grabResult.GetErrorCode()}, " + f"{grabResult.GetErrorDescription()}." + ) + ) def detector_clamp(value: Union[float, int], max_value: int) -> int: From 13622e9d83114d55877bf74ab9367c1717baac81 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+bmoneke@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:28:56 +0200 Subject: [PATCH 3/8] Get latest version --- .../plugins_2D/daq_2Dviewer_GenericPylablibCamera.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py index d60e2c1..b04a845 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py +++ b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py @@ -1,8 +1,3 @@ -""" -Copied (and slightly modified) from https://github.com/rgeneaux/pymodaq_plugins_test_pylablib -""" - - from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, Axis, DataToExport from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main From ca17375db7c02169e716d43a07d0a6dfddde2f2e Mon Sep 17 00:00:00 2001 From: rgeneaux Date: Fri, 21 Feb 2025 14:53:13 +0100 Subject: [PATCH 4/8] Adds compatibility with different attribute names for exposure and gain. ROI functionality now compatible with pymodaq>=4.4 --- README.rst | 1 + plugin_info.toml | 2 +- .../plugins_2D/daq_2Dviewer_Basler.py | 20 +++-------- src/pymodaq_plugins_basler/hardware/basler.py | 34 ++++++++++++++++--- .../daq_2Dviewer_GenericPylablibCamera.py | 15 ++++---- 5 files changed, 42 insertions(+), 30 deletions(-) rename src/pymodaq_plugins_basler/{daq_viewer_plugins/plugins_2D => hardware}/daq_2Dviewer_GenericPylablibCamera.py (97%) diff --git a/README.rst b/README.rst index f93c9bb..a42a099 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,7 @@ Authors ======= * Benedikt Burger +* Romain Geneaux Instruments diff --git a/plugin_info.toml b/plugin_info.toml index 35e86b7..858338a 100644 --- a/plugin_info.toml +++ b/plugin_info.toml @@ -11,7 +11,7 @@ license = 'MIT' [plugin-install] #packages required for your plugin: packages-required = [ - 'pymodaq>=4.1.0', + 'pymodaq>=4.4.7', 'numpy', # for Basler camera 'pypylon', ] diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py index c4b99ad..d538516 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py +++ b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py @@ -4,13 +4,7 @@ from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.control_modules.viewer_utility_classes import main -try: - from pymodaq_plugins_pylablib_camera.daq_viewer_plugins.plugins_2D.daq_2Dviewer_GenericPylablibCamera import DAQ_2DViewer_GenericPylablibCamera - # available here: https://github.com/rgeneaux/pymodaq_plugins_test_pylablib -except ModuleNotFoundError: - # Fall back to the internal version - from pymodaq_plugins_basler.daq_viewer_plugins.plugins_2D.daq_2Dviewer_GenericPylablibCamera import DAQ_2DViewer_GenericPylablibCamera - +from pymodaq_plugins_basler.hardware.daq_2Dviewer_GenericPylablibCamera import DAQ_2DViewer_GenericPylablibCamera from pymodaq_plugins_basler.hardware.basler import DartCamera @@ -20,16 +14,10 @@ class DAQ_2DViewer_Basler(DAQ_2DViewer_GenericPylablibCamera): controller: DartCamera live_mode_available = True - # Generate a **list** of available cameras. - # Two cases: - # 1) Some pylablib classes have a .list_cameras method, which returns a list of available cameras, so we can just use that - # 2) Other classes have a .get_cameras_number(), which returns the number of connected cameras - # in this case we can define the list as self.camera_list = [*range(number_of_cameras)] - # For Basler, this returns a list of friendly names camera_list = [cam.GetFriendlyName() for cam in DartCamera.list_cameras()] - # Update the params (nothing to change here) + # Update the params params = DAQ_2DViewer_GenericPylablibCamera.params + [ {'title': 'Automatic exposure:', 'name': 'auto_exposure', 'type': 'bool', 'value': False}, {'title': 'Gain (dB)', 'name': 'gain', 'type': 'float', 'value': 0, 'limits': [0, 18]}, @@ -107,7 +95,7 @@ def commit_settings(self, param: Parameter) -> None: self.controller.camera.ExposureAuto.SetValue( "Continuous" if self.settings['auto_exposure'] else "Off") elif param.name() == "gain": - self.controller.camera.Gain.SetValue(param.value()) + getattr(self.controller.camera, self.controller.gain_name).SetValue(param.value()) else: super().commit_settings(param=param) @@ -134,4 +122,4 @@ def callback(self, array) -> None: if __name__ == '__main__': - main(__file__) + main(__file__, init=False) diff --git a/src/pymodaq_plugins_basler/hardware/basler.py b/src/pymodaq_plugins_basler/hardware/basler.py index b2dd980..0b53e90 100644 --- a/src/pymodaq_plugins_basler/hardware/basler.py +++ b/src/pymodaq_plugins_basler/hardware/basler.py @@ -37,6 +37,9 @@ def __init__(self, name: str, callback: Optional[Callable] = None, **kwargs): # create camera object self.tlFactory = pylon.TlFactory.GetInstance() self.camera = pylon.InstantCamera() + self.exposure_name = "" + self.gain_name = "" + # register configuration event handler self.configurationEventHandler = ConfigurationHandler() self.camera.RegisterConfiguration( @@ -61,6 +64,26 @@ def open(self, name: str) -> None: self.camera.Attach(device) self.camera.Open() self.attributes["PixelWidth"] = self.pixel_length + self.check_attribute_names() + + def check_attribute_names(self): + possible_exposures = ["ExposureTime", "ExposureTimeAbs"] + for exp in possible_exposures: + try: + if hasattr(self.camera, exp): + self.exposure_name = exp + break + except pylon.LogicalErrorException: + pass + + possible_gains = ["Gain", "GainRaw"] + for gain in possible_gains: + try: + if hasattr(self.camera, gain): + self.gain_name = gain + break + except pylon.LogicalErrorException: + pass def set_callback( self, callback: Callable[[NDArray], None], replace_all: bool = True @@ -105,11 +128,11 @@ def get_device_info(self) -> List[Any]: def get_exposure(self) -> float: """Get the exposure time in s.""" - return self.camera.ExposureTime.GetValue() / 1e6 + return getattr(self.camera, self.exposure_name).GetValue() / 1e6 def set_exposure(self, value: float) -> None: """Set the exposure time in s.""" - self.camera.ExposureTime.SetValue(value * 1e6) + getattr(self.camera, self.exposure_name).SetValue(value * 1e6) def get_roi(self) -> Tuple[float, float, float, float, int, int]: """Return x0, width, y0, height, xbin, ybin.""" @@ -165,7 +188,7 @@ def wait_for_frame( raise NotImplementedError("Not implemented") def get_all_attributes(self): - self.attributes + return self.attributes def get_attribute_value(self, name, error_on_missing=True): """Get the camera attribute with the given name""" @@ -216,7 +239,10 @@ def start_grabbing(self, max_frame_rate: int = 1000) -> None: Whenever a grab succeeded, the callback defined in :meth:`set_callback` is called. """ - self.camera.AcquisitionFrameRate.SetValue(max_frame_rate) + try: + self.camera.AcquisitionFrameRate.SetValue(max_frame_rate) + except pylon.LogicalErrorException: + pass self.camera.StartGrabbing( pylon.GrabStrategy_LatestImageOnly, pylon.GrabLoop_ProvidedByInstantCamera ) diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py b/src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py similarity index 97% rename from src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py rename to src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py index b04a845..28cd5e4 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py +++ b/src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py @@ -6,7 +6,6 @@ from time import perf_counter import numpy as np - class DAQ_2DViewer_GenericPylablibCamera(DAQ_Viewer_base): """ IMPORTANT: THIS IS A GENERIC CLASS THAT DOES NOT WORK ON ITS OWN! @@ -27,13 +26,13 @@ class DAQ_2DViewer_GenericPylablibCamera(DAQ_Viewer_base): {'title': 'Image width', 'name': 'hdet', 'type': 'int', 'value': 1, 'readonly': True, 'default': 1}, {'title': 'Image height', 'name': 'vdet', 'type': 'int', 'value': 1, 'readonly': True, 'default': 1}, {'title': 'Timing', 'name': 'timing_opts', 'type': 'group', 'children': - [{'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 1, 'default': 1}, + [{'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 100, 'default': 100}, {'title': 'Compute FPS', 'name': 'fps_on', 'type': 'bool', 'value': True, 'default': True}, {'title': 'FPS', 'name': 'fps', 'type': 'float', 'value': 0.0, 'readonly': True, 'default': 0.0}] } ] callback_signal = QtCore.Signal() - roi_pos_size = QtCore.QRectF(0, 0, 10, 10) + roi_info = None axes = [] def init_controller(self): @@ -70,10 +69,8 @@ def commit_settings(self, param: Parameter): # We handle ROI and binning separately for clarity (old_x, _, old_y, _, xbin, ybin) = self.controller.get_roi() # Get current binning - x0 = self.roi_pos_size.x() - y0 = self.roi_pos_size.y() - width = self.roi_pos_size.width() - height = self.roi_pos_size.height() + y0, x0 = self.roi_info.origin.coordinates + height, width = self.roi_info.size.coordinates # Values need to be rescaled by binning factor and shifted by current x0,y0 to be correct. new_x = (old_x + x0) * xbin @@ -107,8 +104,8 @@ def commit_settings(self, param: Parameter): self.update_rois(new_roi) param.setValue(False) - def ROISelect(self, roi_pos_size): - self.roi_pos_size = roi_pos_size + def roi_select(self, roi_info, ind_viewer): + self.roi_info = roi_info def ini_detector(self, controller=None): """Detector communication initialization From 6d3c058edb4536571d1ed72ceebe5c9333e73aa4 Mon Sep 17 00:00:00 2001 From: rgeneaux Date: Fri, 21 Feb 2025 17:04:36 +0100 Subject: [PATCH 5/8] Fix pixel length functionality. Allow user-defined pixel size. Update readme and version. --- README.rst | 6 +++++- .../plugins_2D/daq_2Dviewer_Basler.py | 9 +++++++++ src/pymodaq_plugins_basler/hardware/basler.py | 12 +++++++++--- .../hardware/daq_2Dviewer_GenericPylablibCamera.py | 11 +++-------- src/pymodaq_plugins_basler/resources/VERSION | 2 +- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index a42a099..08dc98c 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,11 @@ pymodaq_plugins_basler .. image:: https://github.com/BenediktBurger/pymodaq_plugins_basler/actions/workflows/Test.yml/badge.svg :target: https://github.com/BenediktBurger/pymodaq_plugins_basler/actions/workflows/Test.yml -Set of PyMoDAQ plugins for cameras by Basler. +Set of PyMoDAQ plugins for cameras by Basler, using the pypylon library. It handles basic camera functionalities (gain, exposure, ROI). +The data is emitted together with spatial axes corresponding either to pixels or to real-world units (um). The pixel size of different camera model is hardcoded in the hardware/basler.py file. +If the camera model is not specified, the pixel size is set to 1 um and can be changed manually by the user in the interface. + +The plugin was tested using an acA640-120gm camera. It is compatible with PyMoDAQ 4.4.7. Authors ======= diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py index d538516..2645d59 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py +++ b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py @@ -21,6 +21,7 @@ class DAQ_2DViewer_Basler(DAQ_2DViewer_GenericPylablibCamera): params = DAQ_2DViewer_GenericPylablibCamera.params + [ {'title': 'Automatic exposure:', 'name': 'auto_exposure', 'type': 'bool', 'value': False}, {'title': 'Gain (dB)', 'name': 'gain', 'type': 'float', 'value': 0, 'limits': [0, 18]}, + {'title': 'Pixel size (um)', 'name': 'pixel_length', 'type': 'float', 'value': 1, 'default' : 1, 'visible': False}, ] params[next((i for i, item in enumerate(params) if item["name"] == "camera_list"), None)]['limits'] = camera_list # type: ignore @@ -58,6 +59,12 @@ def ini_detector(self, controller=None): self.ini_detector_init(old_controller=controller, new_controller=self.init_controller()) + # Check if pixel length is known + if self.controller.pixel_length is None: + model = self.controller.camera.GetDeviceInfo().GetModelName() + self.emit_status(ThreadCommand('Update_Status', [(f"No pixel length known for camera model '{model}', defaulting to user-chosen one"), 'log'])) + self.settings.child('pixel_length').show() + # Get camera name self.settings.child('camera_info').setValue(self.controller.get_device_info()[1]) @@ -96,6 +103,8 @@ def commit_settings(self, param: Parameter) -> None: "Continuous" if self.settings['auto_exposure'] else "Off") elif param.name() == "gain": getattr(self.controller.camera, self.controller.gain_name).SetValue(param.value()) + elif param.name() == "pixel_length": + self.controller.pixel_length = param.value() else: super().commit_settings(param=param) diff --git a/src/pymodaq_plugins_basler/hardware/basler.py b/src/pymodaq_plugins_basler/hardware/basler.py index 0b53e90..50be3db 100644 --- a/src/pymodaq_plugins_basler/hardware/basler.py +++ b/src/pymodaq_plugins_basler/hardware/basler.py @@ -17,6 +17,8 @@ "daA1280-54um": 3.75, "daA2500-14um": 2.2, "daA3840-45um": 2, + "acA640-120gm": 5.6, + "acA645-100gm": 5.6, } @@ -253,17 +255,21 @@ def stop_grabbing(self) -> None: @property def pixel_length(self) -> float: """Get the pixel length of the camera in µm. - - :raises: KeyError if the pixel length of the specific model is not known + + Returns None if the pixel length of the specific model is not known """ if self._pixel_length is None: model = self.camera.GetDeviceInfo().GetModelName() try: self._pixel_length = pixel_lengths[model] except KeyError: - raise KeyError(f"No pixel length known for camera model '{model}'.") + self._pixel_length = None return self._pixel_length + @pixel_length.setter + def pixel_length(self, value): + self._pixel_length = value + class ConfigurationHandler(pylon.ConfigurationEventHandler): """Handle the configuration events.""" diff --git a/src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py b/src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py index 28cd5e4..7058d97 100644 --- a/src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py +++ b/src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py @@ -40,7 +40,6 @@ def init_controller(self): def ini_attributes(self): self.controller: None - self.pixel_width = None # pixel size in microns self.x_axis = None self.y_axis = None self.last_tick = 0.0 # time counter used to compute FPS @@ -156,10 +155,6 @@ def ini_detector(self, controller=None): self.callback_thread.callback = callback self.callback_thread.start() - # Check if pixel width is available - if 'PixelWidth' in self.controller.get_all_attributes(): - self.pixel_width = self.controller.get_attribute_value('PixelWidth') - self._prepare_view() info = "Initialized camera" @@ -181,12 +176,12 @@ def _prepare_view(self): self.settings.child('vdet').setValue(height) mock_data = np.zeros((width, height)) - if self.pixel_width: # if pixel_width is defined - scaling = self.pixel_width + if self.controller.pixel_length: # if pixel_width is defined + scaling = self.controller.pixel_length unit = 'um' else: scaling = 1 - unit = 'Pxls' + unit = 'pixels' self.x_axis = Axis(offset = vstart * scaling, scaling=scaling * xbin, size=width // xbin, label="X", units=unit, index=0) diff --git a/src/pymodaq_plugins_basler/resources/VERSION b/src/pymodaq_plugins_basler/resources/VERSION index 6da28dd..afaf360 100644 --- a/src/pymodaq_plugins_basler/resources/VERSION +++ b/src/pymodaq_plugins_basler/resources/VERSION @@ -1 +1 @@ -0.1.1 \ No newline at end of file +1.0.0 \ No newline at end of file From 595096ae331a6b92ea8ccf54a1cf8083d101ce3d Mon Sep 17 00:00:00 2001 From: rgeneaux Date: Mon, 24 Feb 2025 13:46:00 +0100 Subject: [PATCH 6/8] getter/setter for gain and exposure instead of getattr --- README.rst | 3 +- .../plugins_2D/daq_2Dviewer_Basler.py | 6 ++-- src/pymodaq_plugins_basler/hardware/basler.py | 28 +++++++++++++------ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 08dc98c..25fb968 100644 --- a/README.rst +++ b/README.rst @@ -59,5 +59,4 @@ Installation instructions ========================= * You need the manufacturer's driver `Pylon `_ for the cameras. -* This package uses the work of a `genergic pylablib camera driver `_, which is not yet available. - Relevant code is included in this package, so no additional installation needed. + diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py index 2645d59..2f9e172 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py +++ b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py @@ -69,7 +69,7 @@ def ini_detector(self, controller=None): self.settings.child('camera_info').setValue(self.controller.get_device_info()[1]) # Set exposure time - self.controller.set_exposure(self.settings.child('timing_opts', 'exposure_time').value() / 1000) + self.controller.exposure = self.settings.child('timing_opts', 'exposure_time').value() / 1000 # FPS visibility self.settings.child('timing_opts', 'fps').setOpts(visible=self.settings.child('timing_opts', 'fps_on').value()) @@ -101,8 +101,10 @@ def commit_settings(self, param: Parameter) -> None: if param.name() == "auto_exposure": self.controller.camera.ExposureAuto.SetValue( "Continuous" if self.settings['auto_exposure'] else "Off") + elif param.name() == "exposure_time": + self.controller.exposure = param.value()/1000 elif param.name() == "gain": - getattr(self.controller.camera, self.controller.gain_name).SetValue(param.value()) + self.controller.gain = param.value() elif param.name() == "pixel_length": self.controller.pixel_length = param.value() else: diff --git a/src/pymodaq_plugins_basler/hardware/basler.py b/src/pymodaq_plugins_basler/hardware/basler.py index 50be3db..3c5afb8 100644 --- a/src/pymodaq_plugins_basler/hardware/basler.py +++ b/src/pymodaq_plugins_basler/hardware/basler.py @@ -39,8 +39,8 @@ def __init__(self, name: str, callback: Optional[Callable] = None, **kwargs): # create camera object self.tlFactory = pylon.TlFactory.GetInstance() self.camera = pylon.InstantCamera() - self.exposure_name = "" - self.gain_name = "" + self._exposure = None + self._gain = None # register configuration event handler self.configurationEventHandler = ConfigurationHandler() @@ -73,7 +73,7 @@ def check_attribute_names(self): for exp in possible_exposures: try: if hasattr(self.camera, exp): - self.exposure_name = exp + self._exposure = getattr(self.camera, exp) break except pylon.LogicalErrorException: pass @@ -82,7 +82,7 @@ def check_attribute_names(self): for gain in possible_gains: try: if hasattr(self.camera, gain): - self.gain_name = gain + self._gain = getattr(self.camera, gain) break except pylon.LogicalErrorException: pass @@ -128,13 +128,25 @@ def get_device_info(self) -> List[Any]: None, ] - def get_exposure(self) -> float: + @property + def exposure(self) -> float: """Get the exposure time in s.""" - return getattr(self.camera, self.exposure_name).GetValue() / 1e6 + return self._exposure.GetValue() / 1e6 - def set_exposure(self, value: float) -> None: + @exposure.setter + def exposure(self, value: float) -> None: """Set the exposure time in s.""" - getattr(self.camera, self.exposure_name).SetValue(value * 1e6) + self._exposure.SetValue(value * 1e6) + + @property + def gain(self) -> float: + """Get the gain""" + return self._gain.GetValue() + + @gain.setter + def gain(self, value: float) -> None: + """Set the gain""" + self._gain.SetValue(value) def get_roi(self) -> Tuple[float, float, float, float, int, int]: """Return x0, width, y0, height, xbin, ybin.""" From 689c38f73c57c8b4e45aa50414ebd1d5240bf100 Mon Sep 17 00:00:00 2001 From: rgeneaux Date: Mon, 24 Feb 2025 18:51:18 +0100 Subject: [PATCH 7/8] Check if gain is in dB or Raw and adapts parameter type. --- .../daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py | 8 +++++++- src/pymodaq_plugins_basler/hardware/basler.py | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py index 2f9e172..01f013a 100644 --- a/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py +++ b/src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py @@ -20,7 +20,7 @@ class DAQ_2DViewer_Basler(DAQ_2DViewer_GenericPylablibCamera): # Update the params params = DAQ_2DViewer_GenericPylablibCamera.params + [ {'title': 'Automatic exposure:', 'name': 'auto_exposure', 'type': 'bool', 'value': False}, - {'title': 'Gain (dB)', 'name': 'gain', 'type': 'float', 'value': 0, 'limits': [0, 18]}, + {'title': 'Gain (dB)', 'name': 'gain', 'type': 'float', 'value': 0},#, 'limits': [0, 18]}, {'title': 'Pixel size (um)', 'name': 'pixel_length', 'type': 'float', 'value': 1, 'default' : 1, 'visible': False}, ] params[next((i for i, item in enumerate(params) if item["name"] == "camera_list"), None)]['limits'] = camera_list # type: ignore @@ -65,6 +65,12 @@ def ini_detector(self, controller=None): self.emit_status(ThreadCommand('Update_Status', [(f"No pixel length known for camera model '{model}', defaulting to user-chosen one"), 'log'])) self.settings.child('pixel_length').show() + # Check gain mode + if self.controller.raw_gain: + self.settings.child('gain').setOpts(type="int") + self.settings.child('gain').setOpts(title="Gain (raw)") + self.settings.child('gain').setValue(self.controller.gain) + # Get camera name self.settings.child('camera_info').setValue(self.controller.get_device_info()[1]) diff --git a/src/pymodaq_plugins_basler/hardware/basler.py b/src/pymodaq_plugins_basler/hardware/basler.py index 3c5afb8..8fcf35f 100644 --- a/src/pymodaq_plugins_basler/hardware/basler.py +++ b/src/pymodaq_plugins_basler/hardware/basler.py @@ -41,6 +41,7 @@ def __init__(self, name: str, callback: Optional[Callable] = None, **kwargs): self.camera = pylon.InstantCamera() self._exposure = None self._gain = None + self.raw_gain = False # register configuration event handler self.configurationEventHandler = ConfigurationHandler() @@ -83,6 +84,9 @@ def check_attribute_names(self): try: if hasattr(self.camera, gain): self._gain = getattr(self.camera, gain) + + if gain == "GainRaw": + self.raw_gain = True break except pylon.LogicalErrorException: pass @@ -139,12 +143,12 @@ def exposure(self, value: float) -> None: self._exposure.SetValue(value * 1e6) @property - def gain(self) -> float: + def gain(self) -> Union[float, int]: """Get the gain""" return self._gain.GetValue() @gain.setter - def gain(self, value: float) -> None: + def gain(self, value: Union[float, int]) -> None: """Set the gain""" self._gain.SetValue(value) From 6bd44837bedf03c95994afbcb1d897de1e7edba3 Mon Sep 17 00:00:00 2001 From: Benedikt Burger Date: Mon, 3 Mar 2025 09:25:01 +0100 Subject: [PATCH 8/8] Add most recent template files. --- .github/workflows/Testbase.yml | 2 +- .github/workflows/python-publish.yml | 3 ++- src/pymodaq_plugins_basler/app/__init__.py | 0 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 src/pymodaq_plugins_basler/app/__init__.py diff --git a/.github/workflows/Testbase.yml b/.github/workflows/Testbase.yml index 8003d68..ed4517b 100644 --- a/.github/workflows/Testbase.yml +++ b/.github/workflows/Testbase.yml @@ -30,7 +30,7 @@ jobs: export QT_DEBUG_PLUGINS=1 pip install flake8 pytest pytest-cov pytest-qt pytest-xdist pytest-xvfb setuptools wheel numpy h5py ${{ inputs.qt5 }} toml pip install pymodaq - pip install -e . + pip install -e . - name: create local pymodaq folder and setting permissions run: | sudo mkdir /etc/.pymodaq diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 6dd38ac..a9d2f56 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -27,11 +27,12 @@ jobs: run: | sudo mkdir /etc/.pymodaq sudo chmod uo+rw /etc/.pymodaq - + - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel + twine check dist/* twine upload dist/* diff --git a/src/pymodaq_plugins_basler/app/__init__.py b/src/pymodaq_plugins_basler/app/__init__.py new file mode 100644 index 0000000..e69de29