diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml new file mode 100644 index 00000000..179c55b1 --- /dev/null +++ b/.github/workflows/publish_docs.yml @@ -0,0 +1,55 @@ +# Workflow to build the docs (with sphinx) and deploy the build to GitHub Pages +# note: parts of this workflow were copied directly from GitHub's suggested workflows +name: Build and deploy docs + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + id: pages + uses: actions/configure-pages@v3 + - name: Install dependencies + working-directory: ./docs + run: | + python -m pip install --upgrade pip + pip install -r requirements-docs.txt + - name: Build the docs + working-directory: ./docs + run: make build + - name: Upload build artifacts + uses: actions/upload-pages-artifact@v1 + with: + path: './docs/build/html' + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/test_pull_requests.yml b/.github/workflows/test_pull_requests.yml index fa338e4f..3ed2f572 100644 --- a/.github/workflows/test_pull_requests.yml +++ b/.github/workflows/test_pull_requests.yml @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black + pip install black==22.12.0 - name: Check code styling with Black run: | black --diff -S -t py39 copylot diff --git a/copylot/gui/gui.py b/copylot/gui/gui.py index 26f30253..2beba975 100644 --- a/copylot/gui/gui.py +++ b/copylot/gui/gui.py @@ -63,7 +63,9 @@ def __init__(self, *args, **kwargs): ) as json_file: self.defaults = json.load(json_file) - except FileNotFoundError: # construct initial defaults.txt fileself.defaults = [3, 6, 25, 100] + except ( + FileNotFoundError + ): # construct initial defaults.txt fileself.defaults = [3, 6, 25, 100] if not os.path.isdir(os.path.join(str(Path.home()), ".coPylot")): os.mkdir(os.path.join(str(Path.home()), ".coPylot")) diff --git a/copylot/hardware/cameras/avt/__init__.py b/copylot/hardware/cameras/avt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/copylot/hardware/cameras/avt/camera.py b/copylot/hardware/cameras/avt/camera.py new file mode 100644 index 00000000..f9613503 --- /dev/null +++ b/copylot/hardware/cameras/avt/camera.py @@ -0,0 +1,115 @@ +import sys +from enum import Enum +from typing import Tuple + +from copylot.hardware.cameras.abstract_camera import AbstractCamera +from copylot.hardware.cameras.avt.vimba import Vimba + + +class BinningMode(Enum): + AVERAGE = "Average" + SUM = "Sum" + + +class AVTCameraException(Exception): + pass + + +class AVTCamera(AbstractCamera): + def __init__(self, nb_camera = 1): + self.nb_camera = nb_camera + + self._exposure_time = None + + # Internal variables used to keep track of the number of + # > total images + # > incomplete frames + # > frames dropped (i.e the caller attempted to get an image but none were in the queue), + # > times a frame was attempted to be placed in the queue but failed because the queue was full + self.all_count = 0 + self.incomplete_count = 0 + self.dropped_count = 0 + self.full_count = 0 + + self._isActivated = False + # self.queue: queue.Queue[Tuple[np.ndarray, float]] = queue.Queue(maxsize=1) + + @property + def temperature(self) -> float: + """Get the device temperature + + Returns + ------- + float + + """ + try: + return self.camera.DeviceTemperature.get() + except Exception as e: + print( + f"Could not get the device temperature using DeviceTemperature: {e}" + ) + raise e + + @property + def exposure_bounds(self): + try: + exposure_bounds = [] + with Vimba.get_instance() as vimba: + for cam in vimba.get_all_cameras(): + with cam: + exposure_bounds.append( + ( + cam.ExposureAutoMin.get() / 1000, + cam.ExposureAutoMax.get() / 1000 + ) + ) + return exposure_bounds + except Exception as e: + print( + f"Could not get exposure using ExposureAutoMin / ExposureAutoMax: {e}" + ) + raise e + + @property + def exposure_time(self): + return self._exposure_time + + @exposure_time.setter + def exposure_time(self, cam_index_and_value: Tuple[int, int]): + camera_index, value = cam_index_and_value + min_exposure, max_exposure = self.exposure_bounds[camera_index] + if min_exposure <= value <= max_exposure: + try: + with Vimba.get_instance() as vimba: + camera = vimba.get_all_cameras()[camera_index] + camera.ExposureTime.set(value * 1000) + + self._exposure_time = camera.ExposureTime.get() / 1000 + + print(f"Exposure time set to {self.exposure_time} ms.") + except Exception as e: + print(f"Failed to set exposure: {e}") + raise e + else: + raise ValueError( + f"value out of range: must be in " + f"[{min_exposure}, {max_exposure}], but value_ms={value}" + ) + + @staticmethod + def acquire_single_frame(): + with Vimba.get_instance() as vimba: + cams = vimba.get_all_cameras() + with cams[0] as cam: + # Acquire single frame synchronously + frame = cam.get_frame() + + return frame + + # def start_acquisition(self): + # with Vimba.get_instance() as vimba: + # cams = vimba.get_all_cameras() + # if self.nb_camera == 1: + # with cams[0] as cam: + diff --git a/copylot/hardware/cameras/avt/demo/__init__.py b/copylot/hardware/cameras/avt/demo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/copylot/hardware/cameras/avt/demo/camera.py b/copylot/hardware/cameras/avt/demo/camera.py new file mode 100644 index 00000000..2d5d0fde --- /dev/null +++ b/copylot/hardware/cameras/avt/demo/camera.py @@ -0,0 +1,23 @@ +from time import perf_counter + +from copylot.hardware.cameras.avt.camera import AVTCamera + + +def main(): + camera = AVTCamera() + + # acquire single frame + start_time = perf_counter() + AVTCamera().acquire_single_frame() + stop_time = perf_counter() + print(stop_time - start_time) + + # get exposure bounds for all available cameras + print(camera.exposure_bounds) + + # check temperature + print(camera.temperature) + + +if __name__ == '__main__': + main() diff --git a/copylot/hardware/cameras/avt/vimba/__init__.py b/copylot/hardware/cameras/avt/vimba/__init__.py new file mode 100644 index 00000000..e75f55dd --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/__init__.py @@ -0,0 +1,128 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +# Suppress 'imported but unused' - Error from static style checker. +# flake8: noqa: F401 + +__version__ = '1.2.1' + +__all__ = [ + 'Vimba', + 'Camera', + 'CameraChangeHandler', + 'CameraEvent', + 'AccessMode', + 'PersistType', + 'Interface', + 'InterfaceType', + 'InterfaceChangeHandler', + 'InterfaceEvent', + 'PixelFormat', + 'Frame', + 'FeatureTypes', + 'FrameHandler', + 'FrameStatus', + 'AllocationMode', + 'Debayer', + 'intersect_pixel_formats', + 'MONO_PIXEL_FORMATS', + 'BAYER_PIXEL_FORMATS', + 'RGB_PIXEL_FORMATS', + 'RGBA_PIXEL_FORMATS', + 'BGR_PIXEL_FORMATS', + 'BGRA_PIXEL_FORMATS', + 'YUV_PIXEL_FORMATS', + 'YCBCR_PIXEL_FORMATS', + 'COLOR_PIXEL_FORMATS', + 'OPENCV_PIXEL_FORMATS', + + 'VimbaSystemError', + 'VimbaCameraError', + 'VimbaInterfaceError', + 'VimbaFeatureError', + 'VimbaFrameError', + 'VimbaTimeout', + + 'IntFeature', + 'FloatFeature', + 'StringFeature', + 'BoolFeature', + 'EnumEntry', + 'EnumFeature', + 'CommandFeature', + 'RawFeature', + + 'LogLevel', + 'LogConfig', + 'Log', + 'LOG_CONFIG_TRACE_CONSOLE_ONLY', + 'LOG_CONFIG_TRACE_FILE_ONLY', + 'LOG_CONFIG_TRACE', + 'LOG_CONFIG_INFO_CONSOLE_ONLY', + 'LOG_CONFIG_INFO_FILE_ONLY', + 'LOG_CONFIG_INFO', + 'LOG_CONFIG_WARNING_CONSOLE_ONLY', + 'LOG_CONFIG_WARNING_FILE_ONLY', + 'LOG_CONFIG_WARNING', + 'LOG_CONFIG_ERROR_CONSOLE_ONLY', + 'LOG_CONFIG_ERROR_FILE_ONLY', + 'LOG_CONFIG_ERROR', + 'LOG_CONFIG_CRITICAL_CONSOLE_ONLY', + 'LOG_CONFIG_CRITICAL_FILE_ONLY', + 'LOG_CONFIG_CRITICAL', + + 'TraceEnable', + 'ScopedLogEnable', + 'RuntimeTypeCheckEnable' +] + +# Import everything exported from the top level module +from .vimba import Vimba + +from .camera import AccessMode, PersistType, Camera, CameraChangeHandler, CameraEvent, FrameHandler + +from .interface import Interface, InterfaceType, InterfaceChangeHandler, InterfaceEvent + +from .frame import PixelFormat, Frame, Debayer, intersect_pixel_formats, MONO_PIXEL_FORMATS, \ + BAYER_PIXEL_FORMATS, RGB_PIXEL_FORMATS, RGBA_PIXEL_FORMATS, BGR_PIXEL_FORMATS, \ + BGRA_PIXEL_FORMATS, YUV_PIXEL_FORMATS, YCBCR_PIXEL_FORMATS, \ + COLOR_PIXEL_FORMATS, OPENCV_PIXEL_FORMATS, FrameStatus, FeatureTypes, \ + AllocationMode + +from .error import VimbaSystemError, VimbaCameraError, VimbaInterfaceError, VimbaFeatureError, \ + VimbaFrameError, VimbaTimeout + +from .feature import IntFeature, FloatFeature, StringFeature, BoolFeature, EnumEntry, EnumFeature, \ + CommandFeature, RawFeature + +from .util import Log, LogLevel, LogConfig, LOG_CONFIG_TRACE_CONSOLE_ONLY, \ + LOG_CONFIG_TRACE_FILE_ONLY, LOG_CONFIG_TRACE, LOG_CONFIG_INFO_CONSOLE_ONLY, \ + LOG_CONFIG_INFO_FILE_ONLY, LOG_CONFIG_INFO, LOG_CONFIG_WARNING_CONSOLE_ONLY, \ + LOG_CONFIG_WARNING_FILE_ONLY, LOG_CONFIG_WARNING, LOG_CONFIG_ERROR_CONSOLE_ONLY, \ + LOG_CONFIG_ERROR_FILE_ONLY, LOG_CONFIG_ERROR, LOG_CONFIG_CRITICAL_CONSOLE_ONLY, \ + LOG_CONFIG_CRITICAL_FILE_ONLY, LOG_CONFIG_CRITICAL, ScopedLogEnable, \ + TraceEnable, RuntimeTypeCheckEnable diff --git a/copylot/hardware/cameras/avt/vimba/c_binding/__init__.py b/copylot/hardware/cameras/avt/vimba/c_binding/__init__.py new file mode 100644 index 00000000..56bdde6f --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/c_binding/__init__.py @@ -0,0 +1,120 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------- + +NOTE: Vimba/Vmb naming convention. +VimbaPython is based heavily on VimbaC, this submodule contains all wrapped types and functions +of VimbaC. All VimbaC Types and Functions are prefixed with 'Vmb', this convention is kept for +all python types interfacing with the C - Layer. VimbaC developers should be able to understand +the interface to VimbaC and keeping the name convention helps a lot in that regard. + +However prefixing everything with 'Vmb' is not required in VimbaPython, therefore most Types +of the public API have no prefix. +""" + +# Suppress 'imported but unused' - Error from static style checker. +# flake8: noqa: F401 + +__all__ = [ + # Exports from vimba_common + 'VmbInt8', + 'VmbUint8', + 'VmbInt16', + 'VmbUint16', + 'VmbInt32', + 'VmbUint32', + 'VmbInt64', + 'VmbUint64', + 'VmbHandle', + 'VmbBool', + 'VmbUchar', + 'VmbDouble', + 'VmbError', + 'VimbaCError', + 'VmbPixelFormat', + 'decode_cstr', + 'decode_flags', + + # Exports from vimba_c + 'VmbInterface', + 'VmbAccessMode', + 'VmbFeatureData', + 'VmbFeaturePersist', + 'VmbFeatureVisibility', + 'VmbFeatureFlags', + 'VmbFrameStatus', + 'VmbFrameFlags', + 'VmbVersionInfo', + 'VmbInterfaceInfo', + 'VmbCameraInfo', + 'VmbFeatureInfo', + 'VmbFeatureEnumEntry', + 'VmbFrame', + 'VmbFeaturePersistSettings', + 'G_VIMBA_C_HANDLE', + 'VIMBA_C_VERSION', + 'EXPECTED_VIMBA_C_VERSION', + 'call_vimba_c', + 'build_callback_type', + + # Exports from vimba_image_transform + 'VmbImage', + 'VmbImageInfo', + 'VmbDebayerMode', + 'VmbTransformInfo', + 'VIMBA_IMAGE_TRANSFORM_VERSION', + 'EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION', + 'call_vimba_image_transform', + 'PIXEL_FORMAT_TO_LAYOUT', + 'LAYOUT_TO_PIXEL_FORMAT', + 'PIXEL_FORMAT_CONVERTIBILITY_MAP', + + # Exports from ctypes + 'byref', + 'sizeof', + 'create_string_buffer' +] + +from .vimba_common import VmbInt8, VmbUint8, VmbInt16, VmbUint16, VmbInt32, VmbUint32, \ + VmbInt64, VmbUint64, VmbHandle, VmbBool, VmbUchar, VmbDouble, VmbError, \ + VimbaCError, VmbPixelFormat, decode_cstr, decode_flags, \ + _select_vimba_home + +from .vimba_c import VmbInterface, VmbAccessMode, VmbFeatureData, \ + VmbFeaturePersist, VmbFeatureVisibility, VmbFeatureFlags, VmbFrameStatus, \ + VmbFrameFlags, VmbVersionInfo, VmbInterfaceInfo, VmbCameraInfo, VmbFeatureInfo, \ + VmbFeatureEnumEntry, VmbFrame, VmbFeaturePersistSettings, \ + G_VIMBA_C_HANDLE, EXPECTED_VIMBA_C_VERSION, VIMBA_C_VERSION, call_vimba_c, \ + build_callback_type + +from .vimba_image_transform import VmbImage, VmbImageInfo, VmbDebayerMode, \ + VIMBA_IMAGE_TRANSFORM_VERSION, \ + EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION, VmbTransformInfo, \ + call_vimba_image_transform, PIXEL_FORMAT_TO_LAYOUT, \ + LAYOUT_TO_PIXEL_FORMAT, PIXEL_FORMAT_CONVERTIBILITY_MAP + +from ctypes import byref, sizeof, create_string_buffer diff --git a/copylot/hardware/cameras/avt/vimba/c_binding/vimba_c.py b/copylot/hardware/cameras/avt/vimba/c_binding/vimba_c.py new file mode 100644 index 00000000..5a7d69a0 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/c_binding/vimba_c.py @@ -0,0 +1,772 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import copy +import ctypes +from typing import Callable, Any, Tuple +from ctypes import c_void_p, c_char_p, byref, sizeof, POINTER as c_ptr, c_char_p as c_str +from ..util import TraceEnable +from ..error import VimbaSystemError +from .vimba_common import Uint32Enum, Int32Enum, VmbInt32, VmbUint32, VmbInt64, VmbUint64, \ + VmbHandle, VmbBool, VmbDouble, VmbError, VimbaCError, VmbPixelFormat, \ + fmt_enum_repr, fmt_repr, fmt_flags_repr, load_vimba_lib + +__version__ = None + +__all__ = [ + 'VmbPixelFormat', + 'VmbInterface', + 'VmbAccessMode', + 'VmbFeatureData', + 'VmbFeaturePersist', + 'VmbFeatureVisibility', + 'VmbFeatureFlags', + 'VmbFrameStatus', + 'VmbFrameFlags', + 'VmbVersionInfo', + 'VmbInterfaceInfo', + 'VmbCameraInfo', + 'VmbFeatureInfo', + 'VmbFeatureEnumEntry', + 'VmbFrame', + 'VmbFeaturePersistSettings', + 'G_VIMBA_C_HANDLE', + 'VIMBA_C_VERSION', + 'EXPECTED_VIMBA_C_VERSION', + 'call_vimba_c', + 'build_callback_type' +] + + +# Types +class VmbInterface(Uint32Enum): + """ + Camera Interface Types: + Unknown - Interface is not known to this version of the API + Firewire - 1394 + Ethernet - GigE + Usb - USB 3.0 + CL - Camera Link + CSI2 - CSI-2 + """ + Unknown = 0 + Firewire = 1 + Ethernet = 2 + Usb = 3 + CL = 4 + CSI2 = 5 + + def __str__(self): + return self._name_ + + +class VmbAccessMode(Uint32Enum): + """ + Camera Access Mode: + None_ - No access + Full - Read and write access + Read - Read-only access + Config - Configuration access (GeV) + Lite - Read and write access without feature access (only addresses) + """ + None_ = 0 + Full = 1 + Read = 2 + Config = 4 + Lite = 8 + + def __str__(self): + return self._name_ + + +class VmbFeatureData(Uint32Enum): + """ + Feature Data Types + Unknown - Unknown feature type + Int - 64 bit integer feature + Float - 64 bit floating point feature + Enum - Enumeration feature + String - String feature + Bool - Boolean feature + Command - Command feature + Raw - Raw (direct register access) feature + None_ - Feature with no data + """ + Unknown = 0 + Int = 1 + Float = 2 + Enum = 3 + String = 4 + Bool = 5 + Command = 6 + Raw = 7 + None_ = 8 + + def __str__(self): + return self._name_ + + +class VmbFeaturePersist(Uint32Enum): + """ + Type of features that are to be saved (persisted) to the XML file + when using VmbCameraSettingsSave + + All - Save all features to XML, including look-up tables + Streamable - Save only features marked as streamable, excluding + look-up tables + NoLUT - Save all features except look-up tables (default) + """ + All = 0 + Streamable = 1 + NoLUT = 2 + + def __str__(self): + return self._name_ + + +class VmbFeatureVisibility(Uint32Enum): + """ + Feature Visibility + Unknown - Feature visibility is not known + Beginner - Feature is visible in feature list (beginner level) + Expert - Feature is visible in feature list (expert level) + Guru - Feature is visible in feature list (guru level) + Invisible - Feature is not visible in feature list + """ + Unknown = 0 + Beginner = 1 + Expert = 2 + Guru = 3 + Invisible = 4 + + def __str__(self): + return self._name_ + + +class VmbFeatureFlags(Uint32Enum): + """ + Feature Flags + None_ - No additional information is provided + Read - Static info about read access. + Current status depends on access mode, check with + VmbFeatureAccessQuery() + Write - Static info about write access. + Current status depends on access mode, check with + VmbFeatureAccessQuery() + Volatile - Value may change at any time + ModifyWrite - Value may change after a write + """ + None_ = 0 + Read = 1 + Write = 2 + Undocumented = 4 + Volatile = 8 + ModifyWrite = 16 + + def __str__(self): + return self._name_ + + +class VmbFrameStatus(Int32Enum): + """ + Frame transfer status + Complete - Frame has been completed without errors + Incomplete - Frame could not be filled to the end + TooSmall - Frame buffer was too small + Invalid - Frame buffer was invalid + """ + Complete = 0 + Incomplete = -1 + TooSmall = -2 + Invalid = -3 + + def __str__(self): + return self._name_ + + +class VmbFrameFlags(Uint32Enum): + """ + Frame Flags + None_ - No additional information is provided + Dimension - Frame's dimension is provided + Offset - Frame's offset is provided (ROI) + FrameID - Frame's ID is provided + Timestamp - Frame's timestamp is provided + """ + None_ = 0 + Dimension = 1 + Offset = 2 + FrameID = 4 + Timestamp = 8 + + def __str__(self): + return self._name_ + + +class VmbVersionInfo(ctypes.Structure): + """ + Version Information + Fields: + major - Type: VmbUint32, Info: Major version number + minor - Type: VmbUint32, Info: Minor version number + patch - Type: VmbUint32, Info: Patch version number + """ + _fields_ = [ + ("major", VmbUint32), + ("minor", VmbUint32), + ("patch", VmbUint32) + ] + + def __str__(self): + return '{}.{}.{}'.format(self.major, self.minor, self.patch) + + def __repr__(self): + rep = 'VmbVersionInfo' + rep += '(major=' + repr(self.major) + rep += ',minor=' + repr(self.minor) + rep += ',patch=' + repr(self.patch) + rep += ')' + return rep + + +class VmbInterfaceInfo(ctypes.Structure): + """ + Interface information. Holds read-only information about an interface. + Fields: + interfaceIdString - Type: c_char_p + Info: Unique identifier for each interface + interfaceType - Type: VmbInterface (VmbUint32) + Info: Interface type, see VmbInterface + interfaceName - Type: c_char_p + Info: Interface name, given by transport layer + serialString - Type: c_char_p + Info: Serial number + permittedAccess - Type: VmbAccessMode (VmbUint32) + Info: Used access mode, see VmbAccessMode + """ + _fields_ = [ + ("interfaceIdString", c_char_p), + ("interfaceType", VmbUint32), + ("interfaceName", c_char_p), + ("serialString", c_char_p), + ("permittedAccess", VmbUint32) + ] + + def __repr__(self): + rep = 'VmbInterfaceInfo' + rep += fmt_repr('(interfaceIdString={}', self.interfaceIdString) + rep += fmt_enum_repr(',interfaceType={}', VmbInterface, self.interfaceType) + rep += fmt_repr(',interfaceName={}', self.interfaceName) + rep += fmt_repr(',serialString={}', self.serialString) + rep += fmt_flags_repr(',permittedAccess={}', VmbAccessMode, self.permittedAccess) + rep += ')' + return rep + + +class VmbCameraInfo(ctypes.Structure): + """ + Camera information. Holds read-only information about a camera. + Fields: + cameraIdString - Type: c_char_p + Info: Unique identifier for each camera + cameraName - Type: c_char_p + Info: Name of the camera + modelName - Type: c_char_p + Info: Model name + serialString - Type: c_char_p + Info: Serial number + permittedAccess - Type: VmbAccessMode (VmbUint32) + Info: Used access mode, see VmbAccessMode + interfaceIdString - Type: c_char_p + Info: Unique value for each interface or bus + """ + _fields_ = [ + ("cameraIdString", c_char_p), + ("cameraName", c_char_p), + ("modelName", c_char_p), + ("serialString", c_char_p), + ("permittedAccess", VmbUint32), + ("interfaceIdString", c_char_p) + ] + + def __repr__(self): + rep = 'VmbCameraInfo' + rep += fmt_repr('(cameraIdString={}', self.cameraIdString) + rep += fmt_repr(',cameraName={}', self.cameraName) + rep += fmt_repr(',modelName={}', self.modelName) + rep += fmt_repr(',serialString={}', self.serialString) + rep += fmt_flags_repr(',permittedAccess={}', VmbAccessMode, self.permittedAccess) + rep += fmt_repr(',interfaceIdString={}', self.interfaceIdString) + rep += ')' + return rep + + +class VmbFeatureInfo(ctypes.Structure): + """ + Feature information. Holds read-only information about a feature. + Fields: + name - Type: c_char_p + Info: Name used in the API + featureDataType - Type: VmbFeatureData (VmbUint32) + Info: Data type of this feature + featureFlags - Type: VmbFeatureFlags (VmbUint32) + Info: Access flags for this feature + category - Type: c_char_p + Info: Category this feature can be found in + displayName - Type: c_char_p + Info: Feature name to be used in GUIs + pollingTime - Type: VmbUint32 + Info: Predefined polling time for volatile + features + unit - Type: c_char_p + Info: Measuring unit as given in the XML file + representation - Type: c_char_p + Info: Representation of a numeric feature + visibility - Type: VmbFeatureVisibility (VmbUint32) + Info: GUI visibility + tooltip - Type: c_char_p + Info: Short description, e.g. for a tooltip + description - Type: c_char_p + Info: Longer description + sfncNamespace - Type: c_char_p + Info: Namespace this feature resides in + isStreamable - Type: VmbBool + Info: Indicates if a feature can be stored + to / loaded from a file + hasAffectedFeatures - Type: VmbBool + Info: Indicates if the feature potentially + affects other features + hasSelectedFeatures - Type: VmbBool + Info: Indicates if the feature selects other + features + """ + _fields_ = [ + ("name", c_char_p), + ("featureDataType", VmbUint32), + ("featureFlags", VmbUint32), + ("category", c_char_p), + ("displayName", c_char_p), + ("pollingTime", VmbUint32), + ("unit", c_char_p), + ("representation", c_char_p), + ("visibility", VmbUint32), + ("tooltip", c_char_p), + ("description", c_char_p), + ("sfncNamespace", c_char_p), + ("isStreamable", VmbBool), + ("hasAffectedFeatures", VmbBool), + ("hasSelectedFeatures", VmbBool) + ] + + def __repr__(self): + rep = 'VmbFeatureInfo' + rep += fmt_repr('(name={}', self.name) + rep += fmt_enum_repr(',featureDataType={}', VmbFeatureData, self.featureDataType) + rep += fmt_flags_repr(',featureFlags={}', VmbFeatureFlags, self.featureFlags) + rep += fmt_repr(',category={}', self.category) + rep += fmt_repr(',displayName={}', self.displayName) + rep += fmt_repr(',pollingTime={}', self.pollingTime) + rep += fmt_repr(',unit={}', self.unit) + rep += fmt_repr(',representation={}', self.representation) + rep += fmt_enum_repr(',visibility={}', VmbFeatureVisibility, self.visibility) + rep += fmt_repr(',tooltip={}', self.tooltip) + rep += fmt_repr(',description={}', self.description) + rep += fmt_repr(',sfncNamespace={}', self.sfncNamespace) + rep += fmt_repr(',isStreamable={}', self.isStreamable) + rep += fmt_repr(',hasAffectedFeatures={}', self.hasAffectedFeatures) + rep += fmt_repr(',hasSelectedFeatures={}', self.hasSelectedFeatures) + rep += ')' + return rep + + +class VmbFeatureEnumEntry(ctypes.Structure): + """ + Info about possible entries of an enumeration feature: + Fields: + name - Type: c_char_p + Info: Name used in the API + displayName - Type: c_char_p + Info: Enumeration entry name to be used in GUIs + visibility - Type: VmbFeatureVisibility (VmbUint32) + Info: GUI visibility + tooltip - Type: c_char_p + Info: Short description, e.g. for a tooltip + description - Type: c_char_p + Info: Longer description + sfncNamespace - Type: c_char_p + Info: Namespace this feature resides in + intValue - Type: VmbInt64 + Info: Integer value of this enumeration entry + """ + _fields_ = [ + ("name", c_char_p), + ("displayName", c_char_p), + ("visibility", VmbUint32), + ("tooltip", c_char_p), + ("description", c_char_p), + ("sfncNamespace", c_char_p), + ("intValue", VmbInt64) + ] + + def __repr__(self): + rep = 'VmbFeatureEnumEntry' + rep += fmt_repr('(name={}', self.name) + rep += fmt_repr(',displayName={}', self.displayName) + rep += fmt_enum_repr(',visibility={}', VmbFeatureVisibility, self.visibility) + rep += fmt_repr(',tooltip={}', self.tooltip) + rep += fmt_repr(',description={}', self.description) + rep += fmt_repr(',sfncNamespace={}', self.sfncNamespace) + rep += fmt_repr(',intValue={},', self.intValue) + rep += ')' + return rep + + +class VmbFrame(ctypes.Structure): + """ + Frame delivered by Camera + Fields (in): + buffer - Type: c_void_p + Info: Comprises image and ancillary data + bufferSize - Type: VmbUint32_t + Info: Size of the data buffer + context - Type: c_void_p[4] + Info: 4 void pointers that can be employed by the user + (e.g. for storing handles) + + Fields (out): + receiveStatus - Type: VmbFrameStatus (VmbInt32) + Info: Resulting status of the receive operation + receiveFlags - Type: VmbFrameFlags (VmbUint32) + Info: Flags indicating which additional frame + information is available + imageSize - Type: VmbUint32 + Info: Size of the image data inside the data buffer + ancillarySize - Type: VmbUint32 + Info: Size of the ancillary data inside the + data buffer + pixelFormat - Type: VmbPixelFormat (VmbUint32) + Info: Pixel format of the image + width - Type: VmbUint32 + Info: Width of an image + height - Type: VmbUint32 + Info: Height of an image + offsetX - Type: VmbUint32 + Info: Horizontal offset of an image + offsetY - Type: VmbUint32 + Info: Vertical offset of an image + frameID - Type: VmbUint64 + Info: Unique ID of this frame in this stream + timestamp - Type: VmbUint64 + Info: Timestamp set by the camera + """ + _fields_ = [ + ("buffer", c_void_p), + ("bufferSize", VmbUint32), + ("context", c_void_p * 4), + ("receiveStatus", VmbInt32), + ("receiveFlags", VmbUint32), + ("imageSize", VmbUint32), + ("ancillarySize", VmbUint32), + ("pixelFormat", VmbUint32), + ("width", VmbUint32), + ("height", VmbUint32), + ("offsetX", VmbUint32), + ("offsetY", VmbUint32), + ("frameID", VmbUint64), + ("timestamp", VmbUint64) + ] + + def __repr__(self): + rep = 'VmbFrame' + rep += fmt_repr('(buffer={}', self.buffer) + rep += fmt_repr(',bufferSize={}', self.bufferSize) + rep += fmt_repr(',context={}', self.context) + rep += fmt_enum_repr('receiveStatus: {}', VmbFrameStatus, self.receiveStatus) + rep += fmt_flags_repr(',receiveFlags={}', VmbFrameFlags, self.receiveFlags) + rep += fmt_repr(',imageSize={}', self.imageSize) + rep += fmt_repr(',ancillarySize={}', self.ancillarySize) + rep += fmt_enum_repr(',pixelFormat={}', VmbPixelFormat, self.pixelFormat) + rep += fmt_repr(',width={}', self.width) + rep += fmt_repr(',height={}', self.height) + rep += fmt_repr(',offsetX={}', self.offsetX) + rep += fmt_repr(',offsetY={}', self.offsetY) + rep += fmt_repr(',frameID={}', self.frameID) + rep += fmt_repr(',timestamp={}', self.timestamp) + rep += ')' + return rep + + def deepcopy_skip_ptr(self, memo): + result = VmbFrame() + memo[id(self)] = result + + result.buffer = None + result.bufferSize = 0 + result.context = (None, None, None, None) + + setattr(result, 'receiveStatus', copy.deepcopy(self.receiveStatus, memo)) + setattr(result, 'receiveFlags', copy.deepcopy(self.receiveFlags, memo)) + setattr(result, 'imageSize', copy.deepcopy(self.imageSize, memo)) + setattr(result, 'ancillarySize', copy.deepcopy(self.ancillarySize, memo)) + setattr(result, 'pixelFormat', copy.deepcopy(self.pixelFormat, memo)) + setattr(result, 'width', copy.deepcopy(self.width, memo)) + setattr(result, 'height', copy.deepcopy(self.height, memo)) + setattr(result, 'offsetX', copy.deepcopy(self.offsetX, memo)) + setattr(result, 'offsetY', copy.deepcopy(self.offsetY, memo)) + setattr(result, 'frameID', copy.deepcopy(self.frameID, memo)) + setattr(result, 'timestamp', copy.deepcopy(self.timestamp, memo)) + return result + + +class VmbFeaturePersistSettings(ctypes.Structure): + """ + Parameters determining the operation mode of VmbCameraSettingsSave + and VmbCameraSettingsLoad + Fields: + persistType - Type: VmbFeaturePersist (VmbUint32) + Info: Type of features that are to be saved + maxIterations - Type: VmbUint32 + Info: Number of iterations when loading settings + loggingLevel - Type: VmbUint32 + Info: Determines level of detail for load/save + settings logging + """ + _fields_ = [ + ("persistType", VmbUint32), + ("maxIterations", VmbUint32), + ("loggingLevel", VmbUint32) + ] + + def __repr__(self): + rep = 'VmbFrame' + rep += fmt_enum_repr('(persistType={}', VmbFeaturePersist, self.persistType) + rep += fmt_repr(',maxIterations={}', self.maxIterations) + rep += fmt_repr(',loggingLevel={}', self.loggingLevel) + rep += ')' + return rep + + +G_VIMBA_C_HANDLE = VmbHandle(1) + +VIMBA_C_VERSION = None +EXPECTED_VIMBA_C_VERSION = '1.9.0' + +# For detailed information on the signatures see "VimbaC.h" +# To improve readability, suppress 'E501 line too long (> 100 characters)' +# check of flake8 +_SIGNATURES = { + 'VmbVersionQuery': (VmbError, [c_ptr(VmbVersionInfo), VmbUint32]), + 'VmbStartup': (VmbError, None), + 'VmbShutdown': (None, None), + 'VmbCamerasList': (VmbError, [c_ptr(VmbCameraInfo), VmbUint32, c_ptr(VmbUint32), VmbUint32]), + 'VmbCameraInfoQuery': (VmbError, [c_str, c_ptr(VmbCameraInfo), VmbUint32]), + 'VmbCameraOpen': (VmbError, [c_str, VmbAccessMode, c_ptr(VmbHandle)]), + 'VmbCameraClose': (VmbError, [VmbHandle]), + 'VmbFeaturesList': (VmbError, [VmbHandle, c_ptr(VmbFeatureInfo), VmbUint32, c_ptr(VmbUint32), VmbUint32]), # noqa: E501 + 'VmbFeatureInfoQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbFeatureInfo), VmbUint32]), + 'VmbFeatureListAffected': (VmbError, [VmbHandle, c_str, c_ptr(VmbFeatureInfo), VmbUint32, c_ptr(VmbUint32), VmbUint32]), # noqa: E501 + 'VmbFeatureListSelected': (VmbError, [VmbHandle, c_str, c_ptr(VmbFeatureInfo), VmbUint32, c_ptr(VmbUint32), VmbUint32]), # noqa: E501 + 'VmbFeatureAccessQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbBool), c_ptr(VmbBool)]), + 'VmbFeatureIntGet': (VmbError, [VmbHandle, c_str, c_ptr(VmbInt64)]), + 'VmbFeatureIntSet': (VmbError, [VmbHandle, c_str, VmbInt64]), + 'VmbFeatureIntRangeQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbInt64), c_ptr(VmbInt64)]), # noqa: E501 + 'VmbFeatureIntIncrementQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbInt64)]), + 'VmbFeatureFloatGet': (VmbError, [VmbHandle, c_str, c_ptr(VmbDouble)]), + 'VmbFeatureFloatSet': (VmbError, [VmbHandle, c_str, VmbDouble]), + 'VmbFeatureFloatRangeQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbDouble), c_ptr(VmbDouble)]), + 'VmbFeatureFloatIncrementQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbBool), c_ptr(VmbDouble)]), # noqa: E501 + 'VmbFeatureEnumGet': (VmbError, [VmbHandle, c_str, c_ptr(c_str)]), + 'VmbFeatureEnumSet': (VmbError, [VmbHandle, c_str, c_str]), + 'VmbFeatureEnumRangeQuery': (VmbError, [VmbHandle, c_str, c_ptr(c_str), VmbUint32, c_ptr(VmbUint32)]), # noqa: E501 + 'VmbFeatureEnumIsAvailable': (VmbError, [VmbHandle, c_str, c_str, c_ptr(VmbBool)]), + 'VmbFeatureEnumAsInt': (VmbError, [VmbHandle, c_str, c_str, c_ptr(VmbInt64)]), + 'VmbFeatureEnumAsString': (VmbError, [VmbHandle, c_str, VmbInt64, c_ptr(c_str)]), + 'VmbFeatureEnumEntryGet': (VmbError, [VmbHandle, c_str, c_str, c_ptr(VmbFeatureEnumEntry), VmbUint32]), # noqa: E501 + 'VmbFeatureStringGet': (VmbError, [VmbHandle, c_str, c_str, VmbUint32, c_ptr(VmbUint32)]), # noqa: E501 + 'VmbFeatureStringSet': (VmbError, [VmbHandle, c_str, c_str]), + 'VmbFeatureStringMaxlengthQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbUint32)]), + 'VmbFeatureBoolGet': (VmbError, [VmbHandle, c_str, c_ptr(VmbBool)]), + 'VmbFeatureBoolSet': (VmbError, [VmbHandle, c_str, VmbBool]), + 'VmbFeatureCommandRun': (VmbError, [VmbHandle, c_str]), + 'VmbFeatureCommandIsDone': (VmbError, [VmbHandle, c_str, c_ptr(VmbBool)]), + 'VmbFeatureRawGet': (VmbError, [VmbHandle, c_str, c_str, VmbUint32, c_ptr(VmbUint32)]), + 'VmbFeatureRawSet': (VmbError, [VmbHandle, c_str, c_str, VmbUint32]), + 'VmbFeatureRawLengthQuery': (VmbError, [VmbHandle, c_str, c_ptr(VmbUint32)]), + 'VmbFeatureInvalidationRegister': (VmbError, [VmbHandle, c_str, c_void_p, c_void_p]), # noqa: E501 + 'VmbFeatureInvalidationUnregister': (VmbError, [VmbHandle, c_str, c_void_p]), + 'VmbFrameAnnounce': (VmbError, [VmbHandle, c_ptr(VmbFrame), VmbUint32]), + 'VmbFrameRevoke': (VmbError, [VmbHandle, c_ptr(VmbFrame)]), + 'VmbFrameRevokeAll': (VmbError, [VmbHandle]), + 'VmbCaptureStart': (VmbError, [VmbHandle]), + 'VmbCaptureEnd': (VmbError, [VmbHandle]), + 'VmbCaptureFrameQueue': (VmbError, [VmbHandle, c_ptr(VmbFrame), c_void_p]), + 'VmbCaptureFrameWait': (VmbError, [VmbHandle, c_ptr(VmbFrame), VmbUint32]), + 'VmbCaptureQueueFlush': (VmbError, [VmbHandle]), + 'VmbInterfacesList': (VmbError, [c_ptr(VmbInterfaceInfo), VmbUint32, c_ptr(VmbUint32), VmbUint32]), # noqa: E501 + 'VmbInterfaceOpen': (VmbError, [c_str, c_ptr(VmbHandle)]), + 'VmbInterfaceClose': (VmbError, [VmbHandle]), + 'VmbAncillaryDataOpen': (VmbError, [c_ptr(VmbFrame), c_ptr(VmbHandle)]), + 'VmbAncillaryDataClose': (VmbError, [VmbHandle]), + 'VmbMemoryRead': (VmbError, [VmbHandle, VmbUint64, VmbUint32, c_str, c_ptr(VmbUint32)]), + 'VmbMemoryWrite': (VmbError, [VmbHandle, VmbUint64, VmbUint32, c_str, c_ptr(VmbUint32)]), + 'VmbRegistersRead': (VmbError, [VmbHandle, VmbUint32, c_ptr(VmbUint64), c_ptr(VmbUint64), c_ptr(VmbUint32)]), # noqa: E501 + 'VmbRegistersWrite': (VmbError, [VmbHandle, VmbUint32, c_ptr(VmbUint64), c_ptr(VmbUint64), c_ptr(VmbUint32)]), # noqa: E501 + 'VmbCameraSettingsSave': (VmbError, [VmbHandle, c_str, c_ptr(VmbFeaturePersistSettings), VmbUint32]), # noqa: E501 + 'VmbCameraSettingsLoad': (VmbError, [VmbHandle, c_str, c_ptr(VmbFeaturePersistSettings), VmbUint32]) # noqa: E501 +} + + +def _attach_signatures(lib_handle): + global _SIGNATURES + + for function_name, signature in _SIGNATURES.items(): + fn = getattr(lib_handle, function_name) + fn.restype, fn.argtypes = signature + fn.errcheck = _eval_vmberror + + return lib_handle + + +def _check_version(lib_handle): + global EXPECTED_VIMBA_C_VERSION + global VIMBA_C_VERSION + + v = VmbVersionInfo() + lib_handle.VmbVersionQuery(byref(v), sizeof(v)) + + VIMBA_C_VERSION = str(v) + + loaded_version = (v.major, v.minor, v.patch) + expected_version = tuple(map(int, EXPECTED_VIMBA_C_VERSION.split("."))) + # major and minor version must be equal, patch version may be equal or greater + if not(loaded_version[0:2] == expected_version[0:2] and + loaded_version[2] >= expected_version[2]): + msg = 'Invalid VimbaC Version: Expected: {}, Found:{}' + raise VimbaSystemError(msg.format(EXPECTED_VIMBA_C_VERSION, VIMBA_C_VERSION)) + + return lib_handle + + +def _eval_vmberror(result: VmbError, func: Callable[..., Any], *args: Tuple[Any, ...]): + if result not in (VmbError.Success, None): + raise VimbaCError(result) + + +_lib_instance = _check_version(_attach_signatures(load_vimba_lib('VimbaC'))) + + +@TraceEnable() +def call_vimba_c(func_name: str, *args): + """This function encapsulates the entire VimbaC access. + + For Details on valid function signatures see the 'VimbaC.h'. + + Arguments: + func_name: The function name from VimbaC to be called. + args: Varargs passed directly to the underlaying C-Function. + + Raises: + TypeError if given are do not match the signature of the function. + AttributeError if func with name 'func_name' does not exist. + VimbaCError if the function call is valid but neither None or VmbError.Success was returned. + + The following functions of VimbaC can be executed: + VmbVersionQuery + VmbStartup + VmbShutdown + VmbCamerasList + VmbCameraInfoQuery + VmbCameraOpen + VmbCameraClose + VmbFeaturesList + VmbFeatureInfoQuery + VmbFeatureListAffected + VmbFeatureListSelected + VmbFeatureAccessQuery + VmbFeatureIntGet + VmbFeatureIntSet + VmbFeatureIntRangeQuery + VmbFeatureIntIncrementQuery + VmbFeatureFloatGet + VmbFeatureFloatSet + VmbFeatureFloatRangeQuery + VmbFeatureFloatIncrementQuery + VmbFeatureEnumGet + VmbFeatureEnumSet + VmbFeatureEnumRangeQuery + VmbFeatureEnumIsAvailable + VmbFeatureEnumAsInt + VmbFeatureEnumAsString + VmbFeatureEnumEntryGet + VmbFeatureStringGet + VmbFeatureStringSet + VmbFeatureStringMaxlengthQuery + VmbFeatureBoolGet + VmbFeatureBoolSet + VmbFeatureCommandRun + VmbFeatureCommandIsDone + VmbFeatureRawGet + VmbFeatureRawSet + VmbFeatureRawLengthQuery + VmbFeatureInvalidationRegister + VmbFeatureInvalidationUnregister + VmbFrameAnnounce + VmbFrameRevoke + VmbFrameRevokeAll + VmbCaptureStart + VmbCaptureEnd + VmbCaptureFrameQueue + VmbCaptureFrameWait + VmbCaptureQueueFlush + VmbInterfacesList + VmbInterfaceOpen + VmbInterfaceClose + VmbAncillaryDataOpen + VmbAncillaryDataClose + VmbMemoryRead + VmbMemoryWrite + VmbRegistersRead + VmbRegistersWrite + VmbCameraSettingsSave + VmbCameraSettingsLoad + """ + global _lib_instance + getattr(_lib_instance, func_name)(*args) + + +def build_callback_type(*args): + global _lib_instance + + lib_type = type(_lib_instance) + + if lib_type == ctypes.CDLL: + return ctypes.CFUNCTYPE(*args) + + elif lib_type == ctypes.WinDLL: + return ctypes.WINFUNCTYPE(*args) + + else: + raise VimbaSystemError('Unknown Library Type. Abort.') diff --git a/copylot/hardware/cameras/avt/vimba/c_binding/vimba_common.py b/copylot/hardware/cameras/avt/vimba/c_binding/vimba_common.py new file mode 100644 index 00000000..4fb3a1a9 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/c_binding/vimba_common.py @@ -0,0 +1,611 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import ctypes +import enum +import os +import sys +import platform +import functools +from typing import Tuple, List +from ..error import VimbaSystemError + + +__all__ = [ + 'Int32Enum', + 'Uint32Enum', + 'VmbInt8', + 'VmbUint8', + 'VmbInt16', + 'VmbUint16', + 'VmbInt32', + 'VmbUint32', + 'VmbInt64', + 'VmbUint64', + 'VmbHandle', + 'VmbBool', + 'VmbUchar', + 'VmbFloat', + 'VmbDouble', + 'VmbError', + 'VimbaCError', + 'VmbPixelFormat', + 'decode_cstr', + 'decode_flags', + 'fmt_repr', + 'fmt_enum_repr', + 'fmt_flags_repr', + 'load_vimba_lib' +] + + +# Types +class Int32Enum(enum.IntEnum): + @classmethod + def from_param(cls, obj): + return ctypes.c_int(obj) + + +class Uint32Enum(enum.IntEnum): + @classmethod + def from_param(cls, obj): + return ctypes.c_uint(obj) + + +# Aliases for vmb base types +VmbInt8 = ctypes.c_byte +VmbUint8 = ctypes.c_ubyte +VmbInt16 = ctypes.c_short +VmbUint16 = ctypes.c_ushort +VmbInt32 = ctypes.c_int +VmbUint32 = ctypes.c_uint +VmbInt64 = ctypes.c_longlong +VmbUint64 = ctypes.c_ulonglong +VmbHandle = ctypes.c_void_p +VmbBool = ctypes.c_bool +VmbUchar = ctypes.c_char +VmbFloat = ctypes.c_float +VmbDouble = ctypes.c_double + + +class VmbError(Int32Enum): + """ + Enum containing error types returned + Success - No error + InternalFault - Unexpected fault in VimbaC or driver + ApiNotStarted - VmbStartup() was not called before the current + command + NotFound - The designated instance (camera, feature etc.) + cannot be found + BadHandle - The given handle is not valid + DeviceNotOpen - Device was not opened for usage + InvalidAccess - Operation is invalid with the current access mode + BadParameter - One of the parameters is invalid (usually an illegal + pointer) + StructSize - The given struct size is not valid for this version + of the API + MoreData - More data available in a string/list than space is + provided + WrongType - Wrong feature type for this access function + InvalidValue - The value is not valid; Either out of bounds or not + an increment of the minimum + Timeout - Timeout during wait + Other - Other error + Resources - Resources not available (e.g. memory) + InvalidCall - Call is invalid in the current context (callback) + NoTL - No transport layers are found + NotImplemented_ - API feature is not implemented + NotSupported - API feature is not supported + Incomplete - A multiple registers read or write is partially + completed + IO - low level IO error in transport layer + """ + Success = 0 + InternalFault = -1 + ApiNotStarted = -2 + NotFound = -3 + BadHandle = -4 + DeviceNotOpen = -5 + InvalidAccess = -6 + BadParameter = -7 + StructSize = -8 + MoreData = -9 + WrongType = -10 + InvalidValue = -11 + Timeout = -12 + Other = -13 + Resources = -14 + InvalidCall = -15 + NoTL = -16 + NotImplemented_ = -17 + NotSupported = -18 + Incomplete = -19 + IO = -20 + + def __str__(self): + return self._name_ + + +class _VmbPixel(Uint32Enum): + Mono = 0x01000000 + Color = 0x02000000 + + +class _VmbPixelOccupy(Uint32Enum): + Bit8 = 0x00080000 + Bit10 = 0x000A0000 + Bit12 = 0x000C0000 + Bit14 = 0x000E0000 + Bit16 = 0x00100000 + Bit24 = 0x00180000 + Bit32 = 0x00200000 + Bit48 = 0x00300000 + Bit64 = 0x00400000 + + +class VmbPixelFormat(Uint32Enum): + """ + Enum containing Pixelformats + Mono formats: + Mono8 - Monochrome, 8 bits (PFNC:Mono8) + Mono10 - Monochrome, 10 bits in 16 bits (PFNC:Mono10) + Mono10p - Monochrome, 4x10 bits continuously packed in 40 bits + (PFNC:Mono10p) + Mono12 - Monochrome, 12 bits in 16 bits (PFNC:Mono12) + Mono12Packed - Monochrome, 2x12 bits in 24 bits (GEV:Mono12Packed) + Mono12p - Monochrome, 2x12 bits continuously packed in 24 bits + (PFNC:Mono12p) + Mono14 - Monochrome, 14 bits in 16 bits (PFNC:Mono14) + Mono16 - Monochrome, 16 bits (PFNC:Mono16) + + Bayer formats: + BayerGR8 - Bayer-color, 8 bits, starting with GR line + (PFNC:BayerGR8) + BayerRG8 - Bayer-color, 8 bits, starting with RG line + (PFNC:BayerRG8) + BayerGB8 - Bayer-color, 8 bits, starting with GB line + (PFNC:BayerGB8) + BayerBG8 - Bayer-color, 8 bits, starting with BG line + (PFNC:BayerBG8) + BayerGR10 - Bayer-color, 10 bits in 16 bits, starting with GR + line (PFNC:BayerGR10) + BayerRG10 - Bayer-color, 10 bits in 16 bits, starting with RG + line (PFNC:BayerRG10) + BayerGB10 - Bayer-color, 10 bits in 16 bits, starting with GB + line (PFNC:BayerGB10) + BayerBG10 - Bayer-color, 10 bits in 16 bits, starting with BG + line (PFNC:BayerBG10) + BayerGR12 - Bayer-color, 12 bits in 16 bits, starting with GR + line (PFNC:BayerGR12) + BayerRG12 - Bayer-color, 12 bits in 16 bits, starting with RG + line (PFNC:BayerRG12) + BayerGB12 - Bayer-color, 12 bits in 16 bits, starting with GB + line (PFNC:BayerGB12) + BayerBG12 - Bayer-color, 12 bits in 16 bits, starting with BG + line (PFNC:BayerBG12) + BayerGR12Packed - Bayer-color, 2x12 bits in 24 bits, starting with GR + line (GEV:BayerGR12Packed) + BayerRG12Packed - Bayer-color, 2x12 bits in 24 bits, starting with RG + line (GEV:BayerRG12Packed) + BayerGB12Packed - Bayer-color, 2x12 bits in 24 bits, starting with GB + line (GEV:BayerGB12Packed) + BayerBG12Packed - Bayer-color, 2x12 bits in 24 bits, starting with BG + line (GEV:BayerBG12Packed) + BayerGR10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with GR line (PFNC:BayerGR10p) + BayerRG10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with RG line (PFNC:BayerRG10p) + BayerGB10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with GB line (PFNC:BayerGB10p) + BayerBG10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with BG line (PFNC:BayerBG10p) + BayerGR12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with GR line (PFNC:BayerGR12p) + BayerRG12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with RG line (PFNC:BayerRG12p) + BayerGB12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with GB line (PFNC:BayerGB12p) + BayerBG12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with BG line (PFNC:BayerBG12p) + BayerGR16 - Bayer-color, 16 bits, starting with GR line + (PFNC:BayerGR16) + BayerRG16 - Bayer-color, 16 bits, starting with RG line + (PFNC:BayerRG16) + BayerGB16 - Bayer-color, 16 bits, starting with GB line + (PFNC:BayerGB16) + BayerBG16 - Bayer-color, 16 bits, starting with BG line + (PFNC:BayerBG16) + + RGB formats: + Rgb8 - RGB, 8 bits x 3 (PFNC:RGB8) + Bgr8 - BGR, 8 bits x 3 (PFNC:Bgr8) + Rgb10 - RGB, 10 bits in 16 bits x 3 (PFNC:RGB10) + Bgr10 - BGR, 10 bits in 16 bits x 3 (PFNC:BGR10) + Rgb12 - RGB, 12 bits in 16 bits x 3 (PFNC:RGB12) + Bgr12 - BGR, 12 bits in 16 bits x 3 (PFNC:BGR12) + Rgb14 - RGB, 14 bits in 16 bits x 3 (PFNC:RGB14) + Bgr14 - BGR, 14 bits in 16 bits x 3 (PFNC:BGR14) + Rgb16 - RGB, 16 bits x 3 (PFNC:RGB16) + Bgr16 - BGR, 16 bits x 3 (PFNC:BGR16) + + RGBA formats: + Argb8 - ARGB, 8 bits x 4 (PFNC:RGBa8) + Rgba8 - RGBA, 8 bits x 4, legacy name + Bgra8 - BGRA, 8 bits x 4 (PFNC:BGRa8) + Rgba10 - RGBA, 10 bits in 16 bits x 4 + Bgra10 - BGRA, 10 bits in 16 bits x 4 + Rgba12 - RGBA, 12 bits in 16 bits x 4 + Bgra12 - BGRA, 12 bits in 16 bits x 4 + Rgba14 - RGBA, 14 bits in 16 bits x 4 + Bgra14 - BGRA, 14 bits in 16 bits x 4 + Rgba16 - RGBA, 16 bits x 4 + Bgra16 - BGRA, 16 bits x 4 + + YUV/YCbCr formats: + Yuv411 - YUV 411 with 8 bits (GEV:YUV411Packed) + Yuv422 - YUV 422 with 8 bits (GEV:YUV422Packed) + Yuv444 - YUV 444 with 8 bits (GEV:YUV444Packed) + YCbCr411_8_CbYYCrYY - Y´CbCr 411 with 8 bits + (PFNC:YCbCr411_8_CbYYCrYY) - identical to Yuv411 + YCbCr422_8_CbYCrY - Y´CbCr 422 with 8 bits + (PFNC:YCbCr422_8_CbYCrY) - identical to Yuv422 + YCbCr8_CbYCr - Y´CbCr 444 with 8 bits + (PFNC:YCbCr8_CbYCr) - identical to Yuv444 + """ + None_ = 0 + Mono8 = _VmbPixel.Mono | _VmbPixelOccupy.Bit8 | 0x0001 + Mono10 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0003 + Mono10p = _VmbPixel.Mono | _VmbPixelOccupy.Bit10 | 0x0046 + Mono12 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0005 + Mono12Packed = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x0006 + Mono12p = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x0047 + Mono14 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0025 + Mono16 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0007 + BayerGR8 = _VmbPixel.Mono | _VmbPixelOccupy.Bit8 | 0x0008 + BayerRG8 = _VmbPixel.Mono | _VmbPixelOccupy.Bit8 | 0x0009 + BayerGB8 = _VmbPixel.Mono | _VmbPixelOccupy.Bit8 | 0x000A + BayerBG8 = _VmbPixel.Mono | _VmbPixelOccupy.Bit8 | 0x000B + BayerGR10 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x000C + BayerRG10 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x000D + BayerGB10 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x000E + BayerBG10 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x000F + BayerGR12 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0010 + BayerRG12 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0011 + BayerGB12 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0012 + BayerBG12 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0013 + BayerGR12Packed = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x002A + BayerRG12Packed = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x002B + BayerGB12Packed = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x002C + BayerBG12Packed = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x002D + BayerGR10p = _VmbPixel.Mono | _VmbPixelOccupy.Bit10 | 0x0056 + BayerRG10p = _VmbPixel.Mono | _VmbPixelOccupy.Bit10 | 0x0058 + BayerGB10p = _VmbPixel.Mono | _VmbPixelOccupy.Bit10 | 0x0054 + BayerBG10p = _VmbPixel.Mono | _VmbPixelOccupy.Bit10 | 0x0052 + BayerGR12p = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x0057 + BayerRG12p = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x0059 + BayerGB12p = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x0055 + BayerBG12p = _VmbPixel.Mono | _VmbPixelOccupy.Bit12 | 0x0053 + BayerGR16 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x002E + BayerRG16 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x002F + BayerGB16 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0030 + BayerBG16 = _VmbPixel.Mono | _VmbPixelOccupy.Bit16 | 0x0031 + Rgb8 = _VmbPixel.Color | _VmbPixelOccupy.Bit24 | 0x0014 + Bgr8 = _VmbPixel.Color | _VmbPixelOccupy.Bit24 | 0x0015 + Rgb10 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x0018 + Bgr10 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x0019 + Rgb12 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x001A + Bgr12 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x001B + Rgb14 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x005E + Bgr14 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x004A + Rgb16 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x0033 + Bgr16 = _VmbPixel.Color | _VmbPixelOccupy.Bit48 | 0x004B + Argb8 = _VmbPixel.Color | _VmbPixelOccupy.Bit32 | 0x0016 + Rgba8 = Argb8 + Bgra8 = _VmbPixel.Color | _VmbPixelOccupy.Bit32 | 0x0017 + Rgba10 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x005F + Bgra10 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x004C + Rgba12 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x0061 + Bgra12 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x004E + Rgba14 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x0063 + Bgra14 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x0050 + Rgba16 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x0064 + Bgra16 = _VmbPixel.Color | _VmbPixelOccupy.Bit64 | 0x0051 + Yuv411 = _VmbPixel.Color | _VmbPixelOccupy.Bit12 | 0x001E + Yuv422 = _VmbPixel.Color | _VmbPixelOccupy.Bit16 | 0x001F + Yuv444 = _VmbPixel.Color | _VmbPixelOccupy.Bit24 | 0x0020 + YCbCr411_8_CbYYCrYY = _VmbPixel.Color | _VmbPixelOccupy.Bit12 | 0x003C + YCbCr422_8_CbYCrY = _VmbPixel.Color | _VmbPixelOccupy.Bit16 | 0x0043 + YCbCr8_CbYCr = _VmbPixel.Color | _VmbPixelOccupy.Bit24 | 0x003A + + def __str__(self): + return self._name_ + + +class VimbaCError(Exception): + """Error Type containing an error code from the C-Layer. This error code is highly context + sensitive. All wrapped C-Functions that do not return VmbError.Success or None must + raise a VimbaCError and the surrounding code must deal if the Error is possible. + """ + + def __init__(self, c_error: VmbError): + super().__init__(repr(c_error)) + self.__c_error = c_error + + def __str__(self): + return repr(self) + + def __repr__(self): + return 'VimbaCError({})'.format(repr(self.__c_error)) + + def get_error_code(self) -> VmbError: + """ Get contained Error Code """ + return self.__c_error + + +# Utility Functions +def _split_into_powers_of_two(num: int) -> Tuple[int, ...]: + result = [] + for mask in [1 << i for i in range(32)]: + if mask & num: + result.append(mask) + + if not result: + result.append(0) + + return tuple(result) + + +def _split_flags_into_enum(num: int, enum_type): + return [enum_type(val) for val in _split_into_powers_of_two(num)] + + +def _repr_flags_list(enum_type, flag_val: int): + values = _split_flags_into_enum(flag_val, enum_type) + + if values: + def fold_func(acc, arg): + return '{} {}'.format(acc, repr(arg)) + + return functools.reduce(fold_func, values, '') + + else: + return '{}'.format(repr(enum_type(0))) + + +def decode_cstr(val: bytes) -> str: + """Converts c_char_p stored in interface structures to a str. + + Arguments: + val - Byte sequence to convert into str. + + Returns: + str represented by 'val' + """ + return val.decode() if val else '' + + +def decode_flags(enum_type, enum_val: int): + """Splits C-styled bit mask into a set of flags from a given Enumeration. + + Arguments: + enum_val - Bit mask to decode. + enum_type - Enum Type represented within 'enum_val' + + Returns: + A set of all values of enum_type occurring in enum_val. + + Raises: + Attribute error a set value is not within the given 'enum_type'. + """ + + return tuple(_split_flags_into_enum(enum_val, enum_type)) + + +def fmt_repr(fmt: str, val): + """Append repr to a format string.""" + return fmt.format(repr(val)) + + +def fmt_enum_repr(fmt: str, enum_type, enum_val): + """Append repr of a given enum type to a format string. + + Arguments: + fmt - Format string + enum_type - Enum Type to construct. + enum_val - Enum value. + + Returns: + formatted string + """ + return fmt.format(repr(enum_type(enum_val))) + + +def fmt_flags_repr(fmt: str, enum_type, enum_val): + """Append repr of a c-style flag value in the form of a set containing + all bits set from a given enum_type. + + Arguments: + fmt - Format string + enum_type - Enum Type to construct. + enum_val - Enum value. + + Returns: + formatted string + """ + return fmt.format(_repr_flags_list(enum_type, enum_val)) + + +def load_vimba_lib(vimba_project: str): + """ Load shared library shipped with the Vimba installation + + Arguments: + vimba_project - Library name without prefix or extension + + Return: + CDLL or WinDLL Handle on loaded library + + Raises: + VimbaSystemError if given library could not be loaded. + """ + + platform_handlers = { + 'linux': _load_under_linux, + 'win32': _load_under_windows + } + + if sys.platform not in platform_handlers: + msg = 'Abort. Unsupported Platform ({}) detected.' + raise VimbaSystemError(msg.format(sys.platform)) + + return platform_handlers[sys.platform](vimba_project) + + +def _load_under_linux(vimba_project: str): + # Construct VimbaHome based on TL installation paths + path_list: List[str] = [] + tl32_path = os.environ.get('GENICAM_GENTL32_PATH', "") + if tl32_path: + path_list += tl32_path.split(':') + tl64_path = os.environ.get('GENICAM_GENTL64_PATH', "") + if tl64_path: + path_list += tl64_path.split(':') + + # Remove empty strings from path_list if there are any. + # Necessary because the GENICAM_GENTLXX_PATH variable might start with a : + path_list = [path for path in path_list if path] + + # Early return if required variables are not set. + if not path_list: + raise VimbaSystemError('No TL detected. Please verify Vimba installation.') + + vimba_home_candidates: List[str] = [] + for path in path_list: + vimba_home = os.path.dirname(os.path.dirname(os.path.dirname(path))) + + if vimba_home not in vimba_home_candidates: + vimba_home_candidates.append(vimba_home) + + # Select the most likely directory from the candidates + vimba_home = _select_vimba_home(vimba_home_candidates) + + arch = platform.machine() + + # Linux x86 64 Bit (Requires additional interpreter version check) + if arch == 'x86_64': + dir_ = 'x86_64bit' if _is_python_64_bit() else 'x86_32bit' + + # Linux x86 32 Bit + elif arch in ('i386', 'i686'): + dir_ = 'x86_32bit' + + # Linux arm 64 Bit (Requires additional interpreter version check) + elif arch == 'aarch64': + dir_ = 'arm_64bit' if _is_python_64_bit() else 'arm_32bit' + + # Linux arm 32 Bit: + elif arch == 'armv7l': + dir_ = 'arm_32bit' + + else: + raise VimbaSystemError('Unknown Architecture \'{}\'. Abort'.format(arch)) + + lib_name = 'lib{}.so'.format(vimba_project) + lib_path = os.path.join(vimba_home, vimba_project, 'DynamicLib', dir_, lib_name) + + try: + lib = ctypes.cdll.LoadLibrary(lib_path) + + except OSError as e: + msg = 'Failed to load library \'{}\'. Please verify Vimba installation.' + raise VimbaSystemError(msg.format(lib_path)) from e + + return lib + + +def _load_under_windows(vimba_project: str): + vimba_home = os.environ.get('VIMBA_HOME') + + if vimba_home is None: + raise VimbaSystemError('Variable VIMBA_HOME not set. Please verify Vimba installation.') + + load_64bit = True if (platform.machine() == 'AMD64') and _is_python_64_bit() else False + lib_name = '{}.dll'.format(vimba_project) + lib_path = os.path.join(vimba_home, vimba_project, 'Bin', 'Win64' if load_64bit else 'Win32', + lib_name) + + try: + # Load Library with 64 Bit and use cdecl call convention + if load_64bit: + lib = ctypes.cdll.LoadLibrary(lib_path) + + # Load Library with 32 Bit and use stdcall call convention + else: + # Tell mypy to ignore this line to allow type checking on both windows and linux as + # windll is not available on linux and would therefore produce an error there + lib = ctypes.windll.LoadLibrary(lib_path) # type: ignore + + except OSError as e: + msg = 'Failed to load library \'{}\'. Please verify Vimba installation.' + raise VimbaSystemError(msg.format(lib_path)) from e + + return lib + + +def _select_vimba_home(candidates: List[str]) -> str: + """ + Select the most likely candidate for VIMBA_HOME from the given list of + candidates + + Arguments: + candidates - List of strings pointing to possible vimba home directories + + Return: + Path that represents the most likely VIMBA_HOME directory + + Raises: + VimbaSystemError if multiple VIMBA_HOME directories were found in candidates + """ + most_likely_candidates = [] + for candidate in candidates: + if 'vimba' in candidate.lower(): + most_likely_candidates.append(candidate) + + if len(most_likely_candidates) == 0: + raise VimbaSystemError('No suitable Vimba installation found. The following paths ' + 'were considered: {}'.format(candidates)) + elif len(most_likely_candidates) > 1: + raise VimbaSystemError('Multiple Vimba installations found. Can\'t decide which to select: ' + '{}'.format(most_likely_candidates)) + + return most_likely_candidates[0] + + +def _is_python_64_bit() -> bool: + # Query if the currently running python interpreter is build as 64 bit binary. + # The default method of getting this information seems to be rather hacky + # (check if maxint > 2^32) but it seems to be the way to do this.... + return True if sys.maxsize > 2**32 else False diff --git a/copylot/hardware/cameras/avt/vimba/c_binding/vimba_image_transform.py b/copylot/hardware/cameras/avt/vimba/c_binding/vimba_image_transform.py new file mode 100644 index 00000000..90c55ac6 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/c_binding/vimba_image_transform.py @@ -0,0 +1,569 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import ctypes +import sys +from ctypes import byref, sizeof, c_char_p, POINTER as c_ptr +from typing import Callable, Any, Tuple, Dict, List + +from ..error import VimbaSystemError +from ..util import TraceEnable +from .vimba_common import Uint32Enum, VmbUint32, VmbInt32, VmbError, VmbFloat, VimbaCError, \ + VmbPixelFormat, load_vimba_lib, fmt_repr, fmt_enum_repr + + +__all__ = [ + 'VmbBayerPattern', + 'VmbEndianness', + 'VmbAligment', + 'VmbAPIInfo', + 'VmbPixelLayout', + 'VmbDebayerMode', + 'VmbImage', + 'VmbImageInfo', + 'VmbTransformInfo', + 'VIMBA_IMAGE_TRANSFORM_VERSION', + 'EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION', + 'call_vimba_image_transform', + 'PIXEL_FORMAT_TO_LAYOUT', + 'LAYOUT_TO_PIXEL_FORMAT', + 'PIXEL_FORMAT_CONVERTIBILITY_MAP' +] + + +class VmbBayerPattern(Uint32Enum): + """Enum defining BayerPatterns + Values: + RGGB - RGGB pattern, red pixel comes first + GBRG - RGGB pattern, green pixel of blue row comes first + GRBG - RGGB pattern, green pixel of red row comes first + BGGR - RGGB pattern, blue pixel comes first + CYGM - CYGM pattern, cyan pixel comes first in the first row, green in the second row + GMCY - CYGM pattern, green pixel comes first in the first row, cyan in the second row + CYMG - CYGM pattern, cyan pixel comes first in the first row, magenta in the second row + MGCY - CYGM pattern, magenta pixel comes first in the first row, cyan in the second row + LAST - Indicator for end of defined range + """ + RGGB = 0 + GBRG = 1 + GRBG = 2 + BGGR = 3 + CYGM = 128 + GMCY = 129 + CYMG = 130 + MGCY = 131 + LAST = 255 + + def __str__(self): + return self._name_ + + +class VmbEndianness(Uint32Enum): + """Enum defining Endian Formats + Values: + LITTLE - Little Endian + BIG - Big Endian + LAST - Indicator for end of defined range + """ + LITTLE = 0 + BIG = 1 + LAST = 255 + + def __str__(self): + return self._name_ + + +class VmbAligment(Uint32Enum): + """Enum defining image alignment + Values: + MSB - Alignment (pppp pppp pppp ....) + LSB - Alignment (.... pppp pppp pppp) + LAST - Indicator for end of defined range + """ + MSB = 0 + LSB = 1 + LAST = 255 + + def __str__(self): + return self._name_ + + +class VmbAPIInfo(Uint32Enum): + """API Info Types + Values: + ALL - All Infos + PLATFORM - Platform the API was built for + BUILD - Build Types (debug or release) + TECHNOLOGY - Special technology info + LAST - Indicator for end of defined range + """ + ALL = 0 + PLATFORM = 1 + BUILD = 2 + TECHNOLOGY = 3 + LAST = 4 + + def __str__(self): + return self._name_ + + +class VmbPixelLayout(Uint32Enum): + """Image Pixel Layout Information. C Header offers no further documentation.""" + Mono = 0 + MonoPacked = 1 + Raw = 2 + RawPacked = 3 + RGB = 4 + BGR = 5 + RGBA = 6 + BGRA = 7 + YUV411 = 8 + YUV422 = 9 + YUV444 = 10 + MonoP = 11 + MonoPl = 12 + RawP = 13 + RawPl = 14 + YYCbYYCr411 = 15 + CbYYCrYY411 = YUV411, + YCbYCr422 = 16 + CbYCrY422 = YUV422 + YCbCr444 = 17 + CbYCr444 = YUV444 + LAST = 19 + + def __str__(self): + return self._name_ + + +class VmbColorSpace(Uint32Enum): + """Image Color space. C Header offers no further documentation.""" + Undefined = 0 + ITU_BT709 = 1 + ITU_BT601 = 2 + + def __str__(self): + return self._name_ + + +class VmbDebayerMode(Uint32Enum): + """Debayer Mode. C Header offers no further documentation.""" + Mode_2x2 = 0 + Mode_3x3 = 1 + Mode_LCAA = 2 + Mode_LCAAV = 3 + Mode_YUV422 = 4 + + def __str__(self): + return self._name_ + + +class VmbTransformType(Uint32Enum): + """TransformType Mode. C Header offers no further documentation.""" + None_ = 0 + DebayerMode = 1 + ColorCorrectionMatrix = 2 + GammaCorrection = 3 + Offset = 4 + Gain = 5 + + def __str__(self): + return self._name_ + + +class VmbPixelInfo(ctypes.Structure): + """Structure containing pixel information. Sadly c_header contains no more documentation""" + _fields_ = [ + ('BitsPerPixel', VmbUint32), + ('BitsUsed', VmbUint32), + ('Alignment', VmbUint32), + ('Endianness', VmbUint32), + ('PixelLayout', VmbUint32), + ('BayerPattern', VmbUint32), + ('Reserved', VmbUint32) + ] + + def __repr__(self): + rep = 'VmbPixelInfo' + rep += fmt_repr('(BitsPerPixel={}', self.BitsPerPixel) + rep += fmt_repr(',BitsUsed={}', self.BitsUsed) + rep += fmt_enum_repr(',Alignment={}', VmbAligment, self.Alignment) + rep += fmt_enum_repr(',Endianness={}', VmbEndianness, self.Endianness) + rep += fmt_enum_repr(',PixelLayout={}', VmbPixelLayout, self.PixelLayout) + rep += fmt_enum_repr(',BayerPattern={}', VmbBayerPattern, self.BayerPattern) + rep += fmt_enum_repr(',Reserved={}', VmbColorSpace, self.Reserved) + rep += ')' + return rep + + +class VmbImageInfo(ctypes.Structure): + """Structure containing image information. Sadly c_header contains no more documentation""" + _fields_ = [ + ('Width', VmbUint32), + ('Height', VmbUint32), + ('Stride', VmbInt32), + ('PixelInfo', VmbPixelInfo) + ] + + def __repr__(self): + rep = 'VmbImageInfo' + rep += fmt_repr('(Width={}', self.Width) + rep += fmt_repr(',Height={}', self.Height) + rep += fmt_repr(',Stride={}', self.Stride) + rep += fmt_repr(',PixelInfo={}', self.PixelInfo) + rep += ')' + return rep + + +class VmbImage(ctypes.Structure): + """Structure containing image. Sadly c_header contains no more documentation""" + _fields_ = [ + ('Size', VmbUint32), + ('Data', ctypes.c_void_p), + ('ImageInfo', VmbImageInfo) + ] + + def __repr__(self): + rep = 'VmbImage' + rep += fmt_repr('(Size={}', self.Size) + rep += fmt_repr(',Data={}', self.Data) + rep += fmt_repr(',ImageInfo={}', self.ImageInfo) + rep += ')' + return rep + + +class VmbTransformParameterMatrix3x3(ctypes.Structure): + """Sadly c_header contains no more documentation""" + _fields_ = [ + ('Matrix', VmbFloat * 9) + ] + + +class VmbTransformParameterGamma(ctypes.Structure): + """Sadly c_header contains no more documentation""" + _fields_ = [ + ('Gamma', VmbFloat) + ] + + +class VmbTransformParameterDebayer(ctypes.Structure): + """Sadly c_header contains no more documentation""" + _fields_ = [ + ('Method', VmbUint32) + ] + + +class VmbTransformParameterOffset(ctypes.Structure): + """Sadly c_header contains no more documentation""" + _fields_ = [ + ('Offset', VmbInt32) + ] + + +class VmbTransformParameterGain(ctypes.Structure): + """Sadly c_header contains no more documentation""" + _fields_ = [ + ('Gain', VmbUint32) + ] + + +class VmbTransformParameter(ctypes.Union): + """Sadly c_header contains no more documentation""" + _fields_ = [ + ('Matrix3x3', VmbTransformParameterMatrix3x3), + ('Debayer', VmbTransformParameterDebayer), + ('Gamma', VmbTransformParameterGamma), + ('Offset', VmbTransformParameterOffset), + ('Gain', VmbTransformParameterGain) + ] + + +class VmbTransformInfo(ctypes.Structure): + """Struct holding transformation information""" + _fields_ = [ + ('TransformType', VmbUint32), + ('Parameter', VmbTransformParameter) + ] + + +# API +VIMBA_IMAGE_TRANSFORM_VERSION = None +if sys.platform == 'linux': + EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION = '1.0' + +else: + EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION = '1.6' + +# For detailed information on the signatures see "VimbaImageTransform.h" +# To improve readability, suppress 'E501 line too long (> 100 characters)' +# check of flake8 +_SIGNATURES = { + 'VmbGetVersion': (VmbError, [c_ptr(VmbUint32)]), + 'VmbGetErrorInfo': (VmbError, [VmbError, c_char_p, VmbUint32]), + 'VmbGetApiInfoString': (VmbError, [VmbAPIInfo, c_char_p, VmbUint32]), + 'VmbSetDebayerMode': (VmbError, [VmbDebayerMode, c_ptr(VmbTransformInfo)]), + 'VmbSetColorCorrectionMatrix3x3': (VmbError, [c_ptr(VmbFloat), c_ptr(VmbTransformInfo)]), + 'VmbSetGammaCorrection': (VmbError, [VmbFloat, c_ptr(VmbTransformInfo)]), + 'VmbSetImageInfoFromPixelFormat': (VmbError, [VmbPixelFormat, VmbUint32, VmbUint32, c_ptr(VmbImage)]), # noqa: E501 + 'VmbSetImageInfoFromString': (VmbError, [c_char_p, VmbUint32, VmbUint32, VmbUint32, c_ptr(VmbImage)]), # noqa: E501 + 'VmbSetImageInfoFromInputParameters': (VmbError, [VmbPixelFormat, VmbUint32, VmbUint32, VmbPixelLayout, VmbUint32, c_ptr(VmbImage)]), # noqa: E501 + 'VmbSetImageInfoFromInputImage': (VmbError, [c_ptr(VmbImage), VmbPixelLayout, VmbUint32, c_ptr(VmbImage)]), # noqa: E501 + 'VmbImageTransform': (VmbError, [c_ptr(VmbImage), c_ptr(VmbImage), c_ptr(VmbTransformInfo), VmbUint32]) # noqa: E501 +} + + +def _attach_signatures(lib_handle): + global _SIGNATURES + + for function_name, signature in _SIGNATURES.items(): + fn = getattr(lib_handle, function_name) + fn.restype, fn.argtypes = signature + fn.errcheck = _eval_vmberror + + return lib_handle + + +def _check_version(lib_handle): + global EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION + global VIMBA_IMAGE_TRANSFORM_VERSION + + v = VmbUint32() + lib_handle.VmbGetVersion(byref(v)) + + VIMBA_IMAGE_TRANSFORM_VERSION = '{}.{}'.format((v.value >> 24) & 0xff, (v.value >> 16) & 0xff) + + loaded_version = tuple(map(int, VIMBA_IMAGE_TRANSFORM_VERSION.split("."))) + expected_version = tuple(map(int, EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION.split("."))) + # Major version must match. minor version may be equal or greater + if not(loaded_version[0] == expected_version[0] and + loaded_version[1] >= expected_version[1]): + msg = 'Invalid VimbaImageTransform Version: Expected: {}, Found:{}' + raise VimbaSystemError(msg.format(EXPECTED_VIMBA_IMAGE_TRANSFORM_VERSION, + VIMBA_IMAGE_TRANSFORM_VERSION)) + + return lib_handle + + +def _eval_vmberror(result: VmbError, func: Callable[..., Any], *args: Tuple[Any, ...]): + if result not in (VmbError.Success, None): + raise VimbaCError(result) + + +_lib_instance = _check_version(_attach_signatures(load_vimba_lib('VimbaImageTransform'))) + + +@TraceEnable() +def call_vimba_image_transform(func_name: str, *args): + """This function encapsulates the entire VimbaImageTransform access. + + For Details on valid function signatures see the 'VimbaImageTransform.h'. + + Arguments: + func_name: The function name from VimbaImageTransform to be called. + args: Varargs passed directly to the underlaying C-Function. + + Raises: + TypeError if given are do not match the signature of the function. + AttributeError if func with name 'func_name' does not exist. + VimbaCError if the function call is valid but neither None or VmbError.Success was returned. + + The following functions of VimbaImageTransform can be executed: + VmbGetVersion + VmbGetTechnoInfo + VmbGetErrorInfo + VmbGetApiInfoString + VmbSetDebayerMode + VmbSetColorCorrectionMatrix3x3 + VmbSetGammaCorrection + VmbSetImageInfoFromPixelFormat + VmbSetImageInfoFromString + VmbSetImageInfoFromInputParameters + VmbSetImageInfoFromInputImage + VmbImageTransform + """ + + global _lib_instance + getattr(_lib_instance, func_name)(*args) + + +PIXEL_FORMAT_TO_LAYOUT: Dict[VmbPixelFormat, Tuple[VmbPixelLayout, int]] = { + VmbPixelFormat.Mono8: (VmbPixelLayout.Mono, 8), + VmbPixelFormat.Mono10: (VmbPixelLayout.Mono, 16), + VmbPixelFormat.Mono12: (VmbPixelLayout.Mono, 16), + VmbPixelFormat.Mono14: (VmbPixelLayout.Mono, 16), + VmbPixelFormat.Mono16: (VmbPixelLayout.Mono, 16), + VmbPixelFormat.BayerGR8: (VmbPixelLayout.Raw, 8), + VmbPixelFormat.BayerRG8: (VmbPixelLayout.Raw, 8), + VmbPixelFormat.BayerGB8: (VmbPixelLayout.Raw, 8), + VmbPixelFormat.BayerBG8: (VmbPixelLayout.Raw, 8), + VmbPixelFormat.BayerGR10: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerRG10: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerGB10: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerBG10: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerGR12: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerRG12: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerGB12: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerBG12: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerGR16: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerRG16: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerGB16: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.BayerBG16: (VmbPixelLayout.Raw, 16), + VmbPixelFormat.Rgb8: (VmbPixelLayout.RGB, 8), + VmbPixelFormat.Rgb10: (VmbPixelLayout.RGB, 16), + VmbPixelFormat.Rgb12: (VmbPixelLayout.RGB, 16), + VmbPixelFormat.Rgb14: (VmbPixelLayout.RGB, 16), + VmbPixelFormat.Rgb16: (VmbPixelLayout.RGB, 16), + VmbPixelFormat.Bgr8: (VmbPixelLayout.BGR, 8), + VmbPixelFormat.Bgr10: (VmbPixelLayout.BGR, 16), + VmbPixelFormat.Bgr12: (VmbPixelLayout.BGR, 16), + VmbPixelFormat.Bgr14: (VmbPixelLayout.BGR, 16), + VmbPixelFormat.Bgr16: (VmbPixelLayout.BGR, 16), + VmbPixelFormat.Rgba8: (VmbPixelLayout.RGBA, 8), + VmbPixelFormat.Rgba10: (VmbPixelLayout.RGBA, 16), + VmbPixelFormat.Rgba12: (VmbPixelLayout.RGBA, 16), + VmbPixelFormat.Rgba14: (VmbPixelLayout.RGBA, 16), + VmbPixelFormat.Rgba16: (VmbPixelLayout.RGBA, 16), + VmbPixelFormat.Bgra8: (VmbPixelLayout.BGRA, 8), + VmbPixelFormat.Bgra10: (VmbPixelLayout.BGRA, 16), + VmbPixelFormat.Bgra12: (VmbPixelLayout.BGRA, 16), + VmbPixelFormat.Bgra14: (VmbPixelLayout.BGRA, 16), + VmbPixelFormat.Bgra16: (VmbPixelLayout.BGRA, 16) +} + +LAYOUT_TO_PIXEL_FORMAT = dict([(v, k) for k, v in PIXEL_FORMAT_TO_LAYOUT.items()]) + + +def _query_compatibility(pixel_format: VmbPixelFormat) -> Tuple[VmbPixelFormat, ...]: + global LAYOUT_TO_PIXEL_FORMAT + + # Query compatible formats from ImageTransform + output_pixel_layouts = (VmbPixelLayout.Mono, VmbPixelLayout.MonoPacked, VmbPixelLayout.Raw, + VmbPixelLayout.RawPacked, VmbPixelLayout.RGB, VmbPixelLayout.BGR, + VmbPixelLayout.RGBA, VmbPixelLayout.BGRA) + + output_bits_per_pixel = (8, 16) + output_layouts = tuple([(layouts, bits) + for layouts in output_pixel_layouts + for bits in output_bits_per_pixel]) + + result: List[VmbPixelFormat] = [] + + src_image = VmbImage() + src_image.Size = sizeof(src_image) + + call_vimba_image_transform('VmbSetImageInfoFromPixelFormat', pixel_format, 0, 0, + byref(src_image)) + + dst_image = VmbImage() + dst_image.Size = sizeof(dst_image) + + for layout, bits in output_layouts: + + try: + call_vimba_image_transform('VmbSetImageInfoFromInputImage', byref(src_image), layout, + bits, byref(dst_image)) + + fmt = LAYOUT_TO_PIXEL_FORMAT[(layout, bits)] + + if fmt not in result: + result.append(fmt) + + except VimbaCError as e: + if e.get_error_code() not in (VmbError.NotImplemented_, VmbError.BadParameter): + raise e + + return tuple(result) + + +PIXEL_FORMAT_CONVERTIBILITY_MAP: Dict[VmbPixelFormat, Tuple[VmbPixelFormat, ...]] = { + VmbPixelFormat.Mono8: _query_compatibility(VmbPixelFormat.Mono8), + VmbPixelFormat.Mono10: _query_compatibility(VmbPixelFormat.Mono10), + VmbPixelFormat.Mono10p: _query_compatibility(VmbPixelFormat.Mono10p), + VmbPixelFormat.Mono12: _query_compatibility(VmbPixelFormat.Mono12), + VmbPixelFormat.Mono12Packed: _query_compatibility(VmbPixelFormat.Mono12Packed), + VmbPixelFormat.Mono12p: _query_compatibility(VmbPixelFormat.Mono12p), + VmbPixelFormat.Mono14: _query_compatibility(VmbPixelFormat.Mono14), + VmbPixelFormat.Mono16: _query_compatibility(VmbPixelFormat.Mono16), + + VmbPixelFormat.BayerGR8: _query_compatibility(VmbPixelFormat.BayerGR8), + VmbPixelFormat.BayerRG8: _query_compatibility(VmbPixelFormat.BayerRG8), + VmbPixelFormat.BayerGB8: _query_compatibility(VmbPixelFormat.BayerGB8), + VmbPixelFormat.BayerBG8: _query_compatibility(VmbPixelFormat.BayerBG8), + VmbPixelFormat.BayerGR10: _query_compatibility(VmbPixelFormat.BayerGR10), + VmbPixelFormat.BayerRG10: _query_compatibility(VmbPixelFormat.BayerRG10), + VmbPixelFormat.BayerGB10: _query_compatibility(VmbPixelFormat.BayerGB10), + VmbPixelFormat.BayerBG10: _query_compatibility(VmbPixelFormat.BayerBG10), + VmbPixelFormat.BayerGR12: _query_compatibility(VmbPixelFormat.BayerGR12), + VmbPixelFormat.BayerRG12: _query_compatibility(VmbPixelFormat.BayerRG12), + VmbPixelFormat.BayerGB12: _query_compatibility(VmbPixelFormat.BayerGB12), + VmbPixelFormat.BayerBG12: _query_compatibility(VmbPixelFormat.BayerBG12), + VmbPixelFormat.BayerGR12Packed: _query_compatibility(VmbPixelFormat.BayerGR12Packed), + VmbPixelFormat.BayerRG12Packed: _query_compatibility(VmbPixelFormat.BayerRG12Packed), + VmbPixelFormat.BayerGB12Packed: _query_compatibility(VmbPixelFormat.BayerGB12Packed), + VmbPixelFormat.BayerBG12Packed: _query_compatibility(VmbPixelFormat.BayerBG12Packed), + VmbPixelFormat.BayerGR10p: _query_compatibility(VmbPixelFormat.BayerGR10p), + VmbPixelFormat.BayerRG10p: _query_compatibility(VmbPixelFormat.BayerRG10p), + VmbPixelFormat.BayerGB10p: _query_compatibility(VmbPixelFormat.BayerGB10p), + VmbPixelFormat.BayerBG10p: _query_compatibility(VmbPixelFormat.BayerBG10p), + VmbPixelFormat.BayerGR12p: _query_compatibility(VmbPixelFormat.BayerGR12p), + VmbPixelFormat.BayerRG12p: _query_compatibility(VmbPixelFormat.BayerRG12p), + VmbPixelFormat.BayerGB12p: _query_compatibility(VmbPixelFormat.BayerGB12p), + VmbPixelFormat.BayerBG12p: _query_compatibility(VmbPixelFormat.BayerBG12p), + VmbPixelFormat.BayerGR16: _query_compatibility(VmbPixelFormat.BayerGR16), + VmbPixelFormat.BayerRG16: _query_compatibility(VmbPixelFormat.BayerRG16), + VmbPixelFormat.BayerGB16: _query_compatibility(VmbPixelFormat.BayerGB16), + VmbPixelFormat.BayerBG16: _query_compatibility(VmbPixelFormat.BayerBG16), + + VmbPixelFormat.Rgb8: _query_compatibility(VmbPixelFormat.Rgb8), + VmbPixelFormat.Bgr8: _query_compatibility(VmbPixelFormat.Bgr8), + VmbPixelFormat.Rgb10: _query_compatibility(VmbPixelFormat.Rgb10), + VmbPixelFormat.Bgr10: _query_compatibility(VmbPixelFormat.Bgr10), + VmbPixelFormat.Rgb12: _query_compatibility(VmbPixelFormat.Rgb12), + VmbPixelFormat.Bgr12: _query_compatibility(VmbPixelFormat.Bgr12), + VmbPixelFormat.Rgb14: _query_compatibility(VmbPixelFormat.Rgb14), + VmbPixelFormat.Bgr14: _query_compatibility(VmbPixelFormat.Bgr14), + VmbPixelFormat.Rgb16: _query_compatibility(VmbPixelFormat.Rgb16), + VmbPixelFormat.Bgr16: _query_compatibility(VmbPixelFormat.Bgr16), + VmbPixelFormat.Argb8: _query_compatibility(VmbPixelFormat.Argb8), + VmbPixelFormat.Rgba8: _query_compatibility(VmbPixelFormat.Rgba8), + VmbPixelFormat.Bgra8: _query_compatibility(VmbPixelFormat.Bgra8), + VmbPixelFormat.Rgba10: _query_compatibility(VmbPixelFormat.Rgba10), + VmbPixelFormat.Bgra10: _query_compatibility(VmbPixelFormat.Bgra10), + VmbPixelFormat.Rgba12: _query_compatibility(VmbPixelFormat.Rgba12), + VmbPixelFormat.Bgra12: _query_compatibility(VmbPixelFormat.Bgra12), + VmbPixelFormat.Rgba14: _query_compatibility(VmbPixelFormat.Rgba14), + VmbPixelFormat.Bgra14: _query_compatibility(VmbPixelFormat.Bgra14), + VmbPixelFormat.Rgba16: _query_compatibility(VmbPixelFormat.Rgba16), + VmbPixelFormat.Bgra16: _query_compatibility(VmbPixelFormat.Bgra16), + + VmbPixelFormat.Yuv411: _query_compatibility(VmbPixelFormat.Yuv411), + VmbPixelFormat.Yuv422: _query_compatibility(VmbPixelFormat.Yuv422), + VmbPixelFormat.Yuv444: _query_compatibility(VmbPixelFormat.Yuv444), + VmbPixelFormat.YCbCr411_8_CbYYCrYY: _query_compatibility(VmbPixelFormat.YCbCr411_8_CbYYCrYY), + VmbPixelFormat.YCbCr422_8_CbYCrY: _query_compatibility(VmbPixelFormat.YCbCr422_8_CbYCrY), + VmbPixelFormat.YCbCr8_CbYCr: _query_compatibility(VmbPixelFormat.YCbCr8_CbYCr) +} diff --git a/copylot/hardware/cameras/avt/vimba/camera.py b/copylot/hardware/cameras/avt/vimba/camera.py new file mode 100644 index 00000000..3acab272 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/camera.py @@ -0,0 +1,1078 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import enum +import os +import copy +import threading + +from ctypes import POINTER +from typing import Tuple, List, Callable, cast, Optional, Union, Dict +from .c_binding import call_vimba_c, build_callback_type, byref, sizeof, decode_cstr, decode_flags +from .c_binding import VmbCameraInfo, VmbHandle, VmbUint32, G_VIMBA_C_HANDLE, VmbAccessMode, \ + VimbaCError, VmbError, VmbFrame, VmbFeaturePersist, VmbFeaturePersistSettings +from .feature import discover_features, discover_feature, FeatureTypes, FeaturesTuple, \ + FeatureTypeTypes +from .shared import filter_features_by_name, filter_features_by_type, filter_affected_features, \ + filter_selected_features, filter_features_by_category, \ + attach_feature_accessors, remove_feature_accessors, read_memory, \ + write_memory, read_registers, write_registers +from .frame import Frame, FormatTuple, PixelFormat, AllocationMode +from .util import Log, TraceEnable, RuntimeTypeCheckEnable, EnterContextOnCall, \ + LeaveContextOnCall, RaiseIfInsideContext, RaiseIfOutsideContext +from .error import VimbaSystemError, VimbaCameraError, VimbaTimeout, VimbaFeatureError + + +__all__ = [ + 'AccessMode', + 'PersistType', + 'FrameHandler', + 'Camera', + 'CameraEvent', + 'CamerasTuple', + 'CamerasList', + 'CameraChangeHandler', + 'discover_cameras', + 'discover_camera' +] + + +# Type Forward declarations +CameraChangeHandler = Callable[['Camera', 'CameraEvent'], None] +CamerasTuple = Tuple['Camera', ...] +CamerasList = List['Camera'] +FrameHandler = Callable[['Camera', Frame], None] + + +class AccessMode(enum.IntEnum): + """Enum specifying all available camera access modes. + + Enum values: + None_ - No access. + Full - Read and write access. Use this mode to configure the camera features and + to acquire images (Camera Link cameras: configuration only). + Read - Read-only access. Setting features is not possible. + Config - Configuration access to configure the IP address of your GigE camera. + """ + None_ = VmbAccessMode.None_ + Full = VmbAccessMode.Full + Read = VmbAccessMode.Read + Config = VmbAccessMode.Config + + +class CameraEvent(enum.IntEnum): + """Enum specifying a Camera Event + + Enum values: + Missing - A known camera disappeared from the bus + Detected - A new camera was discovered + Reachable - A known camera can be accessed + Unreachable - A known camera cannot be accessed anymore + """ + Missing = 0 + Detected = 1 + Reachable = 2 + Unreachable = 3 + + +class PersistType(enum.IntEnum): + """Persistence Type for camera configuration storing and loading. + Enum values: + All - Save all features including lookup tables + Streamable - Save only features tagged with Streamable + NoLUT - Save all features except lookup tables. + """ + All = VmbFeaturePersist.All + Streamable = VmbFeaturePersist.Streamable + NoLUT = VmbFeaturePersist.NoLUT + + +class _Context: + def __init__(self, cam, frames, handler, callback): + self.cam = cam + self.cam_handle = _cam_handle_accessor(cam) + self.frames = frames + self.frames_lock = threading.Lock() + self.frames_handler = handler + self.frames_callback = callback + + +class _State: + def __init__(self, context: _Context): + self.context = context + + +class _StateInit(_State): + @TraceEnable() + def forward(self) -> Union[_State, VimbaCameraError]: + for frame in self.context.frames: + frame_handle = _frame_handle_accessor(frame) + + try: + call_vimba_c('VmbFrameAnnounce', self.context.cam_handle, byref(frame_handle), + sizeof(frame_handle)) + if frame._allocation_mode == AllocationMode.AllocAndAnnounceFrame: + assert frame_handle.buffer is not None + frame._set_buffer(frame_handle.buffer) + + except VimbaCError as e: + return _build_camera_error(self.context.cam, e) + + return _StateAnnounced(self.context) + + +class _StateAnnounced(_State): + @TraceEnable() + def forward(self) -> Union[_State, VimbaCameraError]: + try: + call_vimba_c('VmbCaptureStart', self.context.cam_handle) + + except VimbaCError as e: + return _build_camera_error(self.context.cam, e) + + return _StateCapturing(self.context) + + @TraceEnable() + def backward(self) -> Union[_State, VimbaCameraError]: + for frame in self.context.frames: + frame_handle = _frame_handle_accessor(frame) + + try: + call_vimba_c('VmbFrameRevoke', self.context.cam_handle, byref(frame_handle)) + + except VimbaCError as e: + return _build_camera_error(self.context.cam, e) + + return _StateInit(self.context) + + +class _StateCapturing(_State): + @TraceEnable() + def forward(self) -> Union[_State, VimbaCameraError]: + try: + # Skip Command execution on AccessMode.Read (required for Multicast Streaming) + if self.context.cam.get_access_mode() != AccessMode.Read: + self.context.cam.get_feature_by_name('AcquisitionStart').run() + + except BaseException as e: + return VimbaCameraError(str(e)) + + return _StateAcquiring(self.context) + + @TraceEnable() + def backward(self) -> Union[_State, VimbaCameraError]: + try: + call_vimba_c('VmbCaptureQueueFlush', self.context.cam_handle) + + except VimbaCError as e: + return _build_camera_error(self.context.cam, e) + + return _StateAnnounced(self.context) + + +class _StateNotAcquiring(_State): + @TraceEnable() + def backward(self) -> Union[_State, VimbaCameraError]: + try: + call_vimba_c('VmbCaptureEnd', self.context.cam_handle) + + except VimbaCError as e: + return _build_camera_error(self.context.cam, e) + + return _StateCapturing(self.context) + + +class _StateAcquiring(_State): + @TraceEnable() + def backward(self) -> Union[_State, VimbaCameraError]: + try: + # Skip Command execution on AccessMode.Read (required for Multicast Streaming) + cam = self.context.cam + if cam.get_access_mode() != AccessMode.Read: + cam.get_feature_by_name('AcquisitionStop').run() + + except BaseException as e: + return VimbaCameraError(str(e)) + + return _StateNotAcquiring(self.context) + + @TraceEnable() + def wait_for_frames(self, timeout_ms: int): + for frame in self.context.frames: + self.queue_frame(frame) + + for frame in self.context.frames: + frame_handle = _frame_handle_accessor(frame) + + try: + call_vimba_c('VmbCaptureFrameWait', self.context.cam_handle, byref(frame_handle), + timeout_ms) + + except VimbaCError as e: + raise _build_camera_error(self.context.cam, e) from e + + @TraceEnable() + def queue_frame(self, frame): + frame_handle = _frame_handle_accessor(frame) + + try: + call_vimba_c('VmbCaptureFrameQueue', self.context.cam_handle, byref(frame_handle), + self.context.frames_callback) + + except VimbaCError as e: + raise _build_camera_error(self.context.cam, e) from e + + +class _CaptureFsm: + def __init__(self, context: _Context): + self.__context = context + self.__state = _StateInit(self.__context) + + def get_context(self) -> _Context: + return self.__context + + def enter_capturing_mode(self): + # Forward state machine until the end or an error occurs + exc = None + + while not exc: + try: + state_or_exc = self.__state.forward() + + except AttributeError: + break + + if isinstance(state_or_exc, _State): + self.__state = state_or_exc + + else: + exc = state_or_exc + + return exc + + def leave_capturing_mode(self): + # Revert state machine until the initial state is reached or an error occurs + exc = None + + while not exc: + try: + state_or_exc = self.__state.backward() + + except AttributeError: + break + + if isinstance(state_or_exc, _State): + self.__state = state_or_exc + + else: + exc = state_or_exc + + return exc + + def wait_for_frames(self, timeout_ms: int): + # Wait for Frames only in AcquiringMode + if isinstance(self.__state, _StateAcquiring): + self.__state.wait_for_frames(timeout_ms) + + def queue_frame(self, frame): + # Queue Frame only in AcquiringMode + if isinstance(self.__state, _StateAcquiring): + self.__state.queue_frame(frame) + + +@TraceEnable() +def _frame_generator(cam, limit: Optional[int], timeout_ms: int, allocation_mode: AllocationMode): + if cam.is_streaming(): + raise VimbaCameraError('Operation not supported while streaming.') + + frame_data_size = cam.get_feature_by_name('PayloadSize').get() + frames = (Frame(frame_data_size, allocation_mode), ) + fsm = _CaptureFsm(_Context(cam, frames, None, None)) + cnt = 0 + + try: + while True if limit is None else cnt < limit: + # Enter Capturing mode + exc = fsm.enter_capturing_mode() + if exc: + raise exc + + fsm.wait_for_frames(timeout_ms) + + # Return copy of internally used frame to keep them independent. + frame_copy = copy.deepcopy(frames[0]) + fsm.leave_capturing_mode() + frame_copy._frame.frameID = cnt + cnt += 1 + + yield frame_copy + + finally: + # Leave Capturing mode + exc = fsm.leave_capturing_mode() + if exc: + raise exc + + +class Camera: + """This class allows access to a Camera detected by Vimba. + Camera is meant be used in conjunction with the "with" - statement. + On entering a context, all Camera features are detected and can be accessed within the context. + Static Camera properties like Name and Model can be accessed outside the context. + """ + @TraceEnable() + @LeaveContextOnCall() + def __init__(self, info: VmbCameraInfo): + """Do not call directly. Access Cameras via vimba.Vimba instead.""" + self.__handle: VmbHandle = VmbHandle(0) + self.__info: VmbCameraInfo = info + self.__access_mode: AccessMode = AccessMode.Full + self.__feats: FeaturesTuple = () + self.__context_cnt: int = 0 + self.__capture_fsm: Optional[_CaptureFsm] = None + self._disconnected = False + + @TraceEnable() + def __enter__(self): + if not self.__context_cnt: + self._open() + + self.__context_cnt += 1 + return self + + @TraceEnable() + def __exit__(self, exc_type, exc_value, exc_traceback): + self.__context_cnt -= 1 + + if not self.__context_cnt: + self._close() + + def __str__(self): + return 'Camera(id={})'.format(self.get_id()) + + @RaiseIfInsideContext() + @RuntimeTypeCheckEnable() + def set_access_mode(self, access_mode: AccessMode): + """Set camera access mode. + + Arguments: + access_mode - AccessMode for accessing a Camera. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called inside "with" - statement scope. + """ + self.__access_mode = access_mode + + def get_access_mode(self) -> AccessMode: + """Get current camera access mode""" + return self.__access_mode + + def get_id(self) -> str: + """Get Camera Id, for example, DEV_1AB22C00041B""" + return decode_cstr(self.__info.cameraIdString) + + def get_name(self) -> str: + """Get Camera Name, for example, Allied Vision 1800 U-500m""" + return decode_cstr(self.__info.cameraName) + + def get_model(self) -> str: + """Get Camera Model, for example, 1800 U-500m""" + return decode_cstr(self.__info.modelName) + + def get_serial(self) -> str: + """Get Camera serial number, for example, 50-0503328442""" + return decode_cstr(self.__info.serialString) + + def get_permitted_access_modes(self) -> Tuple[AccessMode, ...]: + """Get a set of all access modes the camera can be accessed with.""" + val = self.__info.permittedAccess + + # Clear VmbAccessMode.Lite Flag. It is offered by VimbaC, but it is not documented. + val &= ~int(VmbAccessMode.Lite) + + return decode_flags(AccessMode, val) + + def get_interface_id(self) -> str: + """Get ID of the Interface this camera is connected to, for example, VimbaUSBInterface_0x0 + """ + return decode_cstr(self.__info.interfaceIdString) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def read_memory(self, addr: int, max_bytes: int) -> bytes: # coverage: skip + """Read a byte sequence from a given memory address. + + Arguments: + addr: Starting address to read from. + max_bytes: Maximum number of bytes to read from addr. + + Returns: + Read memory contents as bytes. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if addr is negative. + ValueError if max_bytes is negative. + ValueError if the memory access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return read_memory(self.__handle, addr, max_bytes) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def write_memory(self, addr: int, data: bytes): # coverage: skip + """Write a byte sequence to a given memory address. + + Arguments: + addr: Address to write the content of 'data' too. + data: Byte sequence to write at address 'addr'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if addr is negative. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return write_memory(self.__handle, addr, data) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def read_registers(self, addrs: Tuple[int, ...]) -> Dict[int, int]: # coverage: skip + """Read contents of multiple registers. + + Arguments: + addrs: Sequence of addresses to be read iteratively. + + Returns: + Dictionary containing a mapping from given address to the read register values. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if any address in addrs is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return read_registers(self.__handle, addrs) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def write_registers(self, addrs_values: Dict[int, int]): # coverage: skip + """Write data to multiple registers. + + Arguments: + addrs_values: Mapping between register addresses and the data to write. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if any address in addrs_values is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return write_registers(self.__handle, addrs_values) + + @RaiseIfOutsideContext() + def get_all_features(self) -> FeaturesTuple: + """Get access to all discovered features of this camera. + + Returns: + A set of all currently detected features. + + Raises: + RuntimeError if called outside "with" - statement scope. + """ + return self.__feats + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_affected_by(self, feat: FeatureTypes) -> FeaturesTuple: + """Get all features affected by a specific camera feature. + + Arguments: + feat - Feature used, find features that are affected by 'feat'. + + Returns: + A set of features affected by changes on 'feat'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + VimbaFeatureError if 'feat' is not a feature of this camera. + """ + return filter_affected_features(self.__feats, feat) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_selected_by(self, feat: FeatureTypes) -> FeaturesTuple: + """Get all features selected by a specific camera feature. + + Arguments: + feat - Feature to find features that are selected by 'feat'. + + Returns: + A feature set selected by changes on 'feat'. + + Raises: + TypeError if 'feat' is not of any feature type. + RuntimeError if called outside "with" - statement scope. + VimbaFeatureError if 'feat' is not a feature of this camera. + """ + return filter_selected_features(self.__feats, feat) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_type(self, feat_type: FeatureTypeTypes) -> FeaturesTuple: + """Get all camera features of a specific feature type. + + Valid FeatureTypes are: IntFeature, FloatFeature, StringFeature, BoolFeature, + EnumFeature, CommandFeature, RawFeature + + Arguments: + feat_type - FeatureType to find features of that type. + + Returns: + A feature set of type 'feat_type'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + """ + return filter_features_by_type(self.__feats, feat_type) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_category(self, category: str) -> FeaturesTuple: + """Get all camera features of a specific category. + + Arguments: + category - Category for filtering features. + + Returns: + A feature set of category 'category'. Can be an empty set if there is + no camera feature of that category. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + """ + return filter_features_by_category(self.__feats, category) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_feature_by_name(self, feat_name: str) -> FeatureTypes: + """Get a camera feature by its name. + + Arguments: + feat_name - Name to find a feature. + + Returns: + Feature with the associated name. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + VimbaFeatureError if no feature is associated with 'feat_name'. + """ + feat = filter_features_by_name(self.__feats, feat_name) + + if not feat: + raise VimbaFeatureError('Feature \'{}\' not found.'.format(feat_name)) + + return feat + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_frame_generator(self, + limit: Optional[int] = None, + timeout_ms: int = 2000, + allocation_mode: AllocationMode = AllocationMode.AnnounceFrame): + """Construct frame generator, providing synchronous image acquisition. + + The Frame generator acquires a new frame with each execution. + + Arguments: + limit - The number of images the generator shall acquire. If limit is None, + the generator will produce an unlimited amount of images and must be + stopped by the user supplied code. + timeout_ms - Timeout in milliseconds of frame acquisition. + allocation_mode - Allocation mode deciding if buffer allocation should be done by + VimbaPython or the Transport Layer + + Returns: + Frame generator expression + + Raises: + RuntimeError if called outside "with" - statement scope. + ValueError if a limit is supplied and negative. + ValueError if a timeout_ms is negative. + VimbaTimeout if Frame acquisition timed out. + VimbaCameraError if Camera is streaming while executing the generator. + """ + if limit and (limit < 0): + raise ValueError('Given Limit {} is not >= 0'.format(limit)) + + if timeout_ms <= 0: + raise ValueError('Given Timeout {} is not > 0'.format(timeout_ms)) + + return _frame_generator(self, limit, timeout_ms, allocation_mode) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_frame(self, + timeout_ms: int = 2000, + allocation_mode: AllocationMode = AllocationMode.AnnounceFrame) -> Frame: + """Get single frame from camera. Synchronous frame acquisition. + + Arguments: + timeout_ms - Timeout in milliseconds of frame acquisition. + allocation_mode - Allocation mode deciding if buffer allocation should be done by + VimbaPython or the Transport Layer + + Returns: + Frame from camera + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if a timeout_ms is negative. + VimbaTimeout if Frame acquisition timed out. + """ + return next(self.get_frame_generator(1, timeout_ms, allocation_mode)) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def start_streaming(self, + handler: FrameHandler, + buffer_count: int = 5, + allocation_mode: AllocationMode = AllocationMode.AnnounceFrame): + """Enter streaming mode + + Enter streaming mode is also known as asynchronous frame acquisition. + While active, the camera acquires and buffers frames continuously. + With each acquired frame, a given FrameHandler is called with a new Frame. + + Arguments: + handler - Callable that is executed on each acquired frame. + buffer_count - Number of frames supplied as internal buffer. + allocation_mode - Allocation mode deciding if buffer allocation should be done by + VimbaPython or the Transport Layer + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if buffer is less or equal to zero. + VimbaCameraError if the camera is already streaming. + VimbaCameraError if anything went wrong on entering streaming mode. + """ + if buffer_count <= 0: + raise ValueError('Given buffer_count {} must be positive'.format(buffer_count)) + + if self.is_streaming(): + raise VimbaCameraError('Camera \'{}\' already streaming.'.format(self.get_id())) + + # Setup capturing fsm + payload_size = self.get_feature_by_name('PayloadSize').get() + frames = tuple([Frame(payload_size, allocation_mode) for _ in range(buffer_count)]) + callback = build_callback_type(None, VmbHandle, POINTER(VmbFrame))(self.__frame_cb_wrapper) + + self.__capture_fsm = _CaptureFsm(_Context(self, frames, handler, callback)) + + # Try to enter streaming mode. If this fails perform cleanup and raise error + exc = self.__capture_fsm.enter_capturing_mode() + if exc: + self.__capture_fsm.leave_capturing_mode() + self.__capture_fsm = None + raise exc + + else: + for frame in frames: + self.__capture_fsm.queue_frame(frame) + + @TraceEnable() + @RaiseIfOutsideContext() + def stop_streaming(self): + """Leave streaming mode. + + Leave asynchronous frame acquisition. If streaming mode was not activated before, + it just returns silently. + + Raises: + RuntimeError if called outside "with" - statement scope. + VimbaCameraError if anything went wrong on leaving streaming mode. + """ + if not self.is_streaming(): + return + + # Leave Capturing mode. If any error occurs, report it and cleanup + try: + exc = self.__capture_fsm.leave_capturing_mode() + if exc: + raise exc + + finally: + self.__capture_fsm = None + + @TraceEnable() + def is_streaming(self) -> bool: + """Returns True if the camera is currently in streaming mode. If not, returns False.""" + return self.__capture_fsm is not None and not self._disconnected + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def queue_frame(self, frame: Frame): + """Reuse acquired frame in streaming mode. + + Add given frame back into the frame queue used in streaming mode. This + should be the last operation on a registered FrameHandler. If streaming mode is not + active, it returns silently. + + Arguments: + frame - The frame to reuse. + + Raises: + TypeError if parameters do not match their type hint. + ValueError if the given frame is not from the internal buffer queue. + RuntimeError if called outside "with" - statement scope. + VimbaCameraError if reusing the frame was unsuccessful. + """ + if self.__capture_fsm is None: + return + + if frame not in self.__capture_fsm.get_context().frames: + raise ValueError('Given Frame is not from Queue') + + self.__capture_fsm.queue_frame(frame) + + @TraceEnable() + @RaiseIfOutsideContext() + def get_pixel_formats(self) -> FormatTuple: + """Get supported pixel formats from Camera. + + Returns: + All pixel formats the camera supports + + Raises: + RuntimeError if called outside "with" - statement scope. + """ + result = [] + feat = self.get_feature_by_name('PixelFormat') + + # Build intersection between PixelFormat Enum Values and PixelFormat + # Note: The Mapping is a bit complicated due to different writing styles within + # Feature EnumEntries and PixelFormats + all_fmts = set([k.upper() for k in PixelFormat.__members__]) + all_enum_fmts = set([str(k).upper() for k in feat.get_available_entries()]) + fmts = all_fmts.intersection(all_enum_fmts) + + for k in PixelFormat.__members__: + if k.upper() in fmts: + result.append(PixelFormat[k]) + + return tuple(result) + + @TraceEnable() + @RaiseIfOutsideContext() + def get_pixel_format(self): + """Get current pixel format. + + Raises: + RuntimeError if called outside "with" - statement scope. + """ + enum_value = str(self.get_feature_by_name('PixelFormat').get()).upper() + + for k in PixelFormat.__members__: + if k.upper() == enum_value: + return PixelFormat[k] + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def set_pixel_format(self, fmt: PixelFormat): + """Set current pixel format. + + Arguments: + fmt - Default pixel format to set. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if the given pixel format is not supported by the cameras. + """ + if fmt not in self.get_pixel_formats(): + raise ValueError('Camera does not support PixelFormat \'{}\''.format(str(fmt))) + + feat = self.get_feature_by_name('PixelFormat') + fmt_str = str(fmt).upper() + + for entry in feat.get_available_entries(): + if str(entry).upper() == fmt_str: + feat.set(entry) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def save_settings(self, file: str, persist_type: PersistType): + """Save camera settings to XML - File + + Arguments: + file - The location for storing the current settings. The given + file must be a file ending with ".xml". + persist_type - Parameter specifying which setting types to store. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if argument path is no ".xml"- File. + """ + + if not file.endswith('.xml'): + raise ValueError('Given file \'{}\' must end with \'.xml\''.format(file)) + + settings = VmbFeaturePersistSettings() + settings.persistType = VmbFeaturePersist(persist_type) + + call_vimba_c('VmbCameraSettingsSave', self.__handle, file.encode('utf-8'), byref(settings), + sizeof(settings)) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def load_settings(self, file: str, persist_type: PersistType): + """Load camera settings from XML file + + Arguments: + file - The location for loading current settings. The given + file must be a file ending with ".xml". + persist_type - Parameter specifying which setting types to load. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement scope. + ValueError if argument path is no ".xml" file. + """ + + if not file.endswith('.xml'): + raise ValueError('Given file \'{}\' must end with \'.xml\''.format(file)) + + if not os.path.exists(file): + raise ValueError('Given file \'{}\' does not exist.'.format(file)) + + settings = VmbFeaturePersistSettings() + settings.persistType = VmbFeaturePersist(persist_type) + + call_vimba_c('VmbCameraSettingsLoad', self.__handle, file.encode('utf-8'), byref(settings), + sizeof(settings)) + + @TraceEnable() + @EnterContextOnCall() + def _open(self): + try: + call_vimba_c('VmbCameraOpen', self.__info.cameraIdString, self.__access_mode, + byref(self.__handle)) + + except VimbaCError as e: + err = e.get_error_code() + + # In theory InvalidAccess should be thrown on using a non permitted access mode. + # In reality VmbError.NotImplemented_ is sometimes returned. + if (err == VmbError.InvalidAccess) or (err == VmbError.NotImplemented_): + msg = 'Accessed Camera \'{}\' with invalid Mode \'{}\'. Valid modes are: {}' + msg = msg.format(self.get_id(), str(self.__access_mode), + self.get_permitted_access_modes()) + exc = VimbaCameraError(msg) + + else: + exc = VimbaCameraError(repr(err)) + + raise exc from e + + self.__feats = discover_features(self.__handle) + attach_feature_accessors(self, self.__feats) + + # Determine current PacketSize (GigE - only) is somewhere between 1500 bytes + feat = filter_features_by_name(self.__feats, 'GVSPPacketSize') + if feat: + try: + min_ = 1400 + max_ = 1600 + size = feat.get() + + if (min_ < size) and (size < max_): + msg = ('Camera {}: GVSPPacketSize not optimized for streaming GigE Vision. ' + 'Enable jumbo packets for improved performance.') + Log.get_instance().info(msg.format(self.get_id())) + + except VimbaFeatureError: + pass + + @TraceEnable() + @LeaveContextOnCall() + def _close(self): + if self.is_streaming(): + self.stop_streaming() + + for feat in self.__feats: + feat.unregister_all_change_handlers() + + remove_feature_accessors(self, self.__feats) + self.__feats = () + + call_vimba_c('VmbCameraClose', self.__handle) + self.__handle = VmbHandle(0) + + def __frame_cb_wrapper(self, _: VmbHandle, raw_frame_ptr: VmbFrame): # coverage: skip + # Skip coverage because it can't be measured. This is called from C-Context. + + # ignore callback if camera has been disconnected + if self.__capture_fsm is None: + return + + context = self.__capture_fsm.get_context() + + with context.frames_lock: + raw_frame = raw_frame_ptr.contents + frame = None + + for f in context.frames: + # Access Frame internals to compare if both point to the same buffer + if raw_frame.buffer == _frame_handle_accessor(f).buffer: + frame = f + break + + # Execute registered handler + assert frame is not None + + try: + context.frames_handler(self, frame) + + except Exception as e: + msg = 'Caught Exception in handler: ' + msg += 'Type: {}, '.format(type(e)) + msg += 'Value: {}, '.format(e) + msg += 'raised by: {}'.format(context.frames_handler) + Log.get_instance().error(msg) + raise e + + +def _setup_network_discovery(): + if discover_feature(G_VIMBA_C_HANDLE, 'GeVTLIsPresent').get(): + discover_feature(G_VIMBA_C_HANDLE, 'GeVDiscoveryAllDuration').set(250) + discover_feature(G_VIMBA_C_HANDLE, 'GeVDiscoveryAllOnce').run() + + +@TraceEnable() +def discover_cameras(network_discovery: bool) -> CamerasList: + """Do not call directly. Access Cameras via vimba.Vimba instead.""" + + if network_discovery: + _setup_network_discovery() + + result = [] + cams_count = VmbUint32(0) + + call_vimba_c('VmbCamerasList', None, 0, byref(cams_count), 0) + + if cams_count: + cams_found = VmbUint32(0) + cams_infos = (VmbCameraInfo * cams_count.value)() + + call_vimba_c('VmbCamerasList', cams_infos, cams_count, byref(cams_found), + sizeof(VmbCameraInfo)) + + for info in cams_infos[:cams_found.value]: + result.append(Camera(info)) + + return result + + +@TraceEnable() +def discover_camera(id_: str) -> Camera: + """Do not call directly. Access Cameras via vimba.Vimba instead.""" + + info = VmbCameraInfo() + + # Try to lookup Camera with given ID. If this function + try: + call_vimba_c('VmbCameraInfoQuery', id_.encode('utf-8'), byref(info), sizeof(info)) + + except VimbaCError as e: + raise VimbaCameraError(str(e.get_error_code())) from e + + return Camera(info) + + +def _cam_handle_accessor(cam: Camera) -> VmbHandle: + # Supress mypi warning. This access is valid although mypi warns about it. + # In this case it is okay to unmangle the name because the raw handle should not be + # exposed. + return cam._Camera__handle # type: ignore + + +def _frame_handle_accessor(frame: Frame) -> VmbFrame: + return frame._frame + + +def _build_camera_error(cam: Camera, orig_exc: VimbaCError) -> VimbaCameraError: + err = orig_exc.get_error_code() + + if err == VmbError.ApiNotStarted: + msg = 'System not ready. \'{}\' accessed outside of system context. Abort.' + exc = cast(VimbaCameraError, VimbaSystemError(msg.format(cam.get_id()))) + + elif err == VmbError.DeviceNotOpen: + msg = 'Camera \'{}\' accessed outside of context. Abort.' + exc = VimbaCameraError(msg.format(cam.get_id())) + + elif err == VmbError.BadHandle: + msg = 'Invalid Camera. \'{}\' might be disconnected. Abort.' + exc = VimbaCameraError(msg.format(cam.get_id())) + + elif err == VmbError.InvalidAccess: + msg = 'Invalid Access Mode on camera \'{}\'. Abort.' + exc = VimbaCameraError(msg.format(cam.get_id())) + + elif err == VmbError.Timeout: + msg = 'Frame capturing on Camera \'{}\' timed out.' + exc = cast(VimbaCameraError, VimbaTimeout(msg.format(cam.get_id()))) + + else: + exc = VimbaCameraError(repr(err)) + + return exc diff --git a/copylot/hardware/cameras/avt/vimba/error.py b/copylot/hardware/cameras/avt/vimba/error.py new file mode 100644 index 00000000..4c0e9306 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/error.py @@ -0,0 +1,95 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from .util import Log + +__all__ = [ + 'VimbaSystemError', + 'VimbaCameraError', + 'VimbaInterfaceError', + 'VimbaFeatureError', + 'VimbaFrameError', + 'VimbaTimeout' +] + + +class _LoggedError(Exception): + def __init__(self, msg: str): + super().__init__(msg) + Log.get_instance().error(msg) + + +class VimbaSystemError(_LoggedError): + """Errors related to the underlying Vimba System + + Error type to indicate system-wide errors like: + - Incomplete Vimba installation + - Incompatible version of the underlying C-Layer + - An unsupported OS + """ + pass + + +class VimbaCameraError(_LoggedError): + """Errors related to cameras + + Error Type to indicated camera-related errors like: + - Access of a disconnected Camera object + - Lookup of non-existing cameras + """ + pass + + +class VimbaInterfaceError(_LoggedError): + """Errors related to Interfaces + + Error Type to indicated interface-related errors like: + - Access on a disconnected Interface object + - Lookup of a non-existing Interface + """ + pass + + +class VimbaFeatureError(_LoggedError): + """Error related to Feature access. + + Error type to indicate invalid Feature access like: + - Invalid access mode on Feature access. + - Out of range values upon setting a value. + - Failed lookup of features. + """ + pass + + +class VimbaFrameError(_LoggedError): + """Error related to Frame data""" + pass + + +class VimbaTimeout(_LoggedError): + """Indicates that an operation timed out.""" + pass diff --git a/copylot/hardware/cameras/avt/vimba/feature.py b/copylot/hardware/cameras/avt/vimba/feature.py new file mode 100644 index 00000000..38229c0f --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/feature.py @@ -0,0 +1,1273 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import inspect +import enum +import ctypes +import threading + +from typing import Tuple, Union, List, Callable, Optional, cast, Type +from .c_binding import call_vimba_c, byref, sizeof, create_string_buffer, decode_cstr, \ + decode_flags, build_callback_type +from .c_binding import VmbFeatureInfo, VmbFeatureFlags, VmbUint32, VmbInt64, VmbHandle, \ + VmbFeatureVisibility, VmbBool, VmbFeatureEnumEntry, VmbFeatureData, \ + VmbError, VimbaCError, VmbDouble + +from .util import Log, TraceEnable, RuntimeTypeCheckEnable +from .error import VimbaFeatureError + +__all__ = [ + 'ChangeHandler', + 'FeatureFlags', + 'FeatureVisibility', + + 'IntFeature', + 'FloatFeature', + 'StringFeature', + 'BoolFeature', + 'EnumEntry', + 'EnumFeature', + 'CommandFeature', + 'RawFeature', + + 'FeatureTypes', + 'FeatureTypeTypes', + 'FeaturesTuple', + 'discover_features', + 'discover_feature', +] + + +ChangeHandler = Callable[['FeatureTypes'], None] + + +class FeatureFlags(enum.IntEnum): + """Enumeration specifying additional information on the feature. + + Enumeration values: + None_ - No additional information is provided + Read - Static info about read access. + Write - Static info about write access. + Volatile - Value may change at any time + ModifyWrite - Value may change after a write + """ + + None_ = VmbFeatureFlags.None_ + Read = VmbFeatureFlags.Read + Write = VmbFeatureFlags.Write + Volatile = VmbFeatureFlags.Volatile + ModifyWrite = VmbFeatureFlags.ModifyWrite + + +class FeatureVisibility(enum.IntEnum): + """Enumeration specifying UI feature visibility. + + Enumeration values: + Unknown - Feature visibility is not known + Beginner - Feature is visible in feature list (beginner level) + Expert - Feature is visible in feature list (expert level) + Guru - Feature is visible in feature list (guru level) + Invisible - Feature is not visible in feature listSu + """ + + Unknown = VmbFeatureVisibility.Unknown + Beginner = VmbFeatureVisibility.Beginner + Expert = VmbFeatureVisibility.Expert + Guru = VmbFeatureVisibility.Guru + Invisible = VmbFeatureVisibility.Invisible + + +class _BaseFeature: + """This class provides most basic feature access functionality. + All FeatureType implementations must derive from BaseFeature. + """ + + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Access Features via System, Camera or Interface Types instead.""" + self._handle: VmbHandle = handle + self._info: VmbFeatureInfo = info + + self.__handlers: List[ChangeHandler] = [] + self.__handlers_lock = threading.Lock() + + CallbackType = build_callback_type(None, VmbHandle, ctypes.c_char_p, ctypes.c_void_p) + self.__feature_callback = CallbackType(self.__feature_cb_wrapper) + + def __repr__(self): + rep = 'Feature' + rep += '(_handle=' + repr(self._handle) + rep += ',_info=' + repr(self._info) + rep += ')' + return rep + + def get_name(self) -> str: + """Get Feature Name, e.g. DiscoveryInterfaceEvent""" + return decode_cstr(self._info.name) + + def get_type(self) -> Type['_BaseFeature']: + """Get Feature Type, e.g. IntFeature""" + return type(self) + + def get_flags(self) -> Tuple[FeatureFlags, ...]: + """Get a set of FeatureFlags, e.g. (FeatureFlags.Read, FeatureFlags.Write))""" + val = self._info.featureFlags + + # The feature flag could contain undocumented values at third bit. + # To prevent any issues, clear the third bit before decoding. + val &= ~4 + + return decode_flags(FeatureFlags, val) + + def get_category(self) -> str: + """Get Feature category, e.g. '/Discovery'""" + return decode_cstr(self._info.category) + + def get_display_name(self) -> str: + """Get lengthy Feature name e.g. 'Discovery Interface Event'""" + return decode_cstr(self._info.displayName) + + def get_polling_time(self) -> int: + """Predefined Polling Time for volatile features.""" + return self._info.pollingTime + + def get_unit(self) -> str: + """Get Unit of this Feature, e.g. 'dB' on Feature 'GainAutoMax'""" + return decode_cstr(self._info.unit) + + def get_representation(self) -> str: + """Representation of a numeric feature.""" + return decode_cstr(self._info.representation) + + def get_visibility(self) -> FeatureVisibility: + """UI visibility of this feature""" + return FeatureVisibility(self._info.visibility) + + def get_tooltip(self) -> str: + """Short Feature description.""" + return decode_cstr(self._info.tooltip) + + def get_description(self) -> str: + """Long feature description.""" + return decode_cstr(self._info.description) + + def get_sfnc_namespace(self) -> str: + """This features namespace""" + return decode_cstr(self._info.sfncNamespace) + + def is_streamable(self) -> bool: + """Indicates if a feature can be stored in /loaded from a file.""" + return self._info.isStreamable + + def has_affected_features(self) -> bool: + """Indicates if this feature can affect other features.""" + return self._info.hasAffectedFeatures + + def has_selected_features(self) -> bool: + """Indicates if this feature selects other features.""" + return self._info.hasSelectedFeatures + + @TraceEnable() + def get_access_mode(self) -> Tuple[bool, bool]: + """Get features current access mode. + + Returns: + A pair of bool. In the first bool is True, read access on this Feature is granted. + If the second bool is True write access on this Feature is granted. + """ + c_read = VmbBool(False) + c_write = VmbBool(False) + + call_vimba_c('VmbFeatureAccessQuery', self._handle, self._info.name, byref(c_read), + byref(c_write)) + + return (c_read.value, c_write.value) + + @TraceEnable() + def is_readable(self) -> bool: + """Is read access on this Features granted? + + Returns: + True if read access is allowed on this feature. False is returned if read access + is not allowed. + """ + r, _ = self.get_access_mode() + return r + + @TraceEnable() + def is_writeable(self) -> bool: + """Is write access on this Features granted? + + Returns: + True if write access is allowed on this feature. False is returned if write access + is not allowed. + """ + _, w = self.get_access_mode() + return w + + @RuntimeTypeCheckEnable() + def register_change_handler(self, handler: ChangeHandler): + """Register Callable on the Feature. + + The Callable will be executed as soon as the Features value changes. The first parameter + on a registered handler will be called with the changed feature itself. The methods + returns early if a given handler is already registered. + + Arguments: + handler - The Callable that should be executed on change. + + Raises: + TypeError if parameters do not match their type hint. + """ + + with self.__handlers_lock: + if handler in self.__handlers: + return + + self.__handlers.append(handler) + + if len(self.__handlers) == 1: + self.__register_callback() + + def unregister_all_change_handlers(self): + """Remove all registered change handlers.""" + with self.__handlers_lock: + if self.__handlers: + self.__unregister_callback() + self.__handlers.clear() + + @RuntimeTypeCheckEnable() + def unregister_change_handler(self, handler: ChangeHandler): + """Remove registered Callable from the Feature. + + Removes a previously registered handler from this Feature. In case the + handler that should be removed was never added in the first place, the method + returns silently. + + Arguments: + handler - The Callable that should be removed. + + Raises: + TypeError if parameters do not match their type hint. + """ + + with self.__handlers_lock: + if handler not in self.__handlers: + return + + if len(self.__handlers) == 1: + self.__unregister_callback() + + self.__handlers.remove(handler) + + @TraceEnable() + def __register_callback(self): + call_vimba_c('VmbFeatureInvalidationRegister', self._handle, self._info.name, + self.__feature_callback, None) + + @TraceEnable() + def __unregister_callback(self): + call_vimba_c('VmbFeatureInvalidationUnregister', self._handle, self._info.name, + self.__feature_callback) + + def __feature_cb_wrapper(self, *_): # coverage: skip + # Skip coverage because it can't be measured. This is called from C-Context. + with self.__handlers_lock: + for handler in self.__handlers: + + try: + handler(self) + + except Exception as e: + msg = 'Caught Exception in handler: ' + msg += 'Type: {}, '.format(type(e)) + msg += 'Value: {}, '.format(e) + msg += 'raised by: {}'.format(handler) + Log.get_instance().error(msg) + raise e + + def _build_access_error(self) -> VimbaFeatureError: + caller_name = inspect.stack()[1][3] + read, write = self.get_access_mode() + + msg = 'Invalid access while calling \'{}()\' of Feature \'{}\'. ' + + msg += 'Read access: {}. '.format('allowed' if read else 'not allowed') + msg += 'Write access: {}. '.format('allowed' if write else 'not allowed') + + return VimbaFeatureError(msg.format(caller_name, self.get_name())) + + def _build_within_callback_error(self) -> VimbaFeatureError: + caller_name = inspect.stack()[1][3] + msg = 'Invalid access. Calling \'{}()\' of Feature \'{}\' in change_handler is invalid.' + + return VimbaFeatureError(msg.format(caller_name, self.get_name())) + + def _build_unhandled_error(self, c_exc: VimbaCError) -> VimbaFeatureError: + return VimbaFeatureError(repr(c_exc.get_error_code())) + + +class BoolFeature(_BaseFeature): + """The BoolFeature is a feature represented by a boolean value.""" + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera, or Interface Types.""" + super().__init__(handle, info) + + def __str__(self): + try: + return 'BoolFeature(name={}, value={})'.format(self.get_name(), self.get()) + + except Exception: + return 'BoolFeature(name={})'.format(self.get_name()) + + @TraceEnable() + def get(self) -> bool: + """Get current feature value of type bool. + + Returns: + Feature value of type bool. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_val = VmbBool(False) + + try: + call_vimba_c('VmbFeatureBoolGet', self._handle, self._info.name, byref(c_val)) + + except VimbaCError as e: + err = e.get_error_code() + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value + + @TraceEnable() + def set(self, val): + """Set current feature value of type bool. + + Arguments: + val - The boolean value to set. + + Raises: + VimbaFeatureError if access rights are not sufficient. + VimbaFeatureError if called with an invalid value. + VimbaFeatureError if executed within a registered change_handler. + """ + as_bool = bool(val) + + try: + call_vimba_c('VmbFeatureBoolSet', self._handle, self._info.name, as_bool) + + except VimbaCError as e: + err = e.get_error_code() + + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + elif err == VmbError.InvalidValue: + exc = self._build_value_error(as_bool) + + elif err == VmbError.InvalidCall: + exc = self._build_within_callback_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + def _build_value_error(self, val: bool) -> VimbaFeatureError: + caller_name = inspect.stack()[1][3] + msg = 'Called \'{}()\' of Feature \'{}\' with invalid value({}).' + + return VimbaFeatureError(msg.format(caller_name, self.get_name(), val)) + + +class CommandFeature(_BaseFeature): + """The CommandFeature is a feature that can perform some kind of operation such as + saving a user set. + """ + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera, or Interface types.""" + super().__init__(handle, info) + + def __str__(self): + return 'CommandFeature(name={})'.format(self.get_name()) + + @TraceEnable() + def run(self): + """Execute command feature. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + try: + call_vimba_c('VmbFeatureCommandRun', self._handle, self._info.name) + + except VimbaCError as e: + exc = cast(VimbaFeatureError, e) + + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + @TraceEnable() + def is_done(self) -> bool: + """Test if a feature execution is done. + + Returns: + True if feature was fully executed. False if the feature is still being executed. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_val = VmbBool(False) + + try: + call_vimba_c('VmbFeatureCommandIsDone', self._handle, self._info.name, byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value + + +class EnumEntry: + """An EnumEntry represents a single value of an EnumFeature. A EnumEntry + is a one-to-one association between a str and an int. + """ + @TraceEnable() + def __init__(self, handle: VmbHandle, feat_name: str, info: VmbFeatureEnumEntry): + """Do not call directly. Instead, access EnumEntries via EnumFeatures.""" + self.__handle: VmbHandle = handle + self.__feat_name: str = feat_name + self.__info: VmbFeatureEnumEntry = info + + def __str__(self): + """Get EnumEntry as str""" + return bytes(self).decode() + + def __int__(self): + """Get EnumEntry as int""" + return self.__info.intValue + + def __bytes__(self): + """Get EnumEntry as bytes""" + return self.__info.name + + def as_tuple(self) -> Tuple[str, int]: + """Get EnumEntry in str and int representation""" + return (str(self), int(self)) + + @TraceEnable() + def is_available(self) -> bool: + """Query if the EnumEntry can currently be used as a value. + + Returns: + True if the EnumEntry can be used as a value, otherwise False. + """ + + c_val = VmbBool(False) + + call_vimba_c('VmbFeatureEnumIsAvailable', self.__handle, self.__feat_name, self.__info.name, + byref(c_val)) + + return c_val.value + + +EnumEntryTuple = Tuple[EnumEntry, ...] + + +class EnumFeature(_BaseFeature): + """The EnumFeature is a feature where only EnumEntry values are allowed. + All possible values of an EnumFeature can be queried through the Feature itself. + """ + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera, or Interface Types.""" + super().__init__(handle, info) + + self.__entries: EnumEntryTuple = _discover_enum_entries(self._handle, self._info.name) + + def __str__(self): + try: + return 'EnumFeature(name={}, value={})'.format(self.get_name(), str(self.get())) + + except Exception: + return 'EnumFeature(name={})'.format(self.get_name()) + + def get_all_entries(self) -> EnumEntryTuple: + """Get a set of all possible EnumEntries of this feature.""" + return self.__entries + + @TraceEnable() + def get_available_entries(self) -> EnumEntryTuple: + """Get a set of all currently available EnumEntries of this feature.""" + return tuple([e for e in self.get_all_entries() if e.is_available()]) + + def get_entry(self, val_or_name: Union[int, str]) -> EnumEntry: + """Get a specific EnumEntry. + + Arguments: + val_or_name: Look up EnumEntry either by its name or its associated value. + + Returns: + EnumEntry associated with Argument 'val_or_name'. + + Raises: + TypeError if int_or_name it not of type int or type str. + VimbaFeatureError if no EnumEntry is associated with 'val_or_name' + """ + for entry in self.__entries: + if type(val_or_name)(entry) == val_or_name: + return entry + + msg = 'EnumEntry lookup failed: No Entry associated with \'{}\'.'.format(val_or_name) + raise VimbaFeatureError(msg) + + @TraceEnable() + def get(self) -> EnumEntry: + """Get current feature value of type EnumEntry. + + Returns: + Feature value of type 'EnumEntry'. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_val = ctypes.c_char_p(None) + + try: + call_vimba_c('VmbFeatureEnumGet', self._handle, self._info.name, byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return self.get_entry(c_val.value.decode() if c_val.value else '') + + @TraceEnable() + def set(self, val: Union[int, str, EnumEntry]): + """Set current feature value of type EnumFeature. + + Arguments: + val - The value to set. Can be int, or str, or EnumEntry. + + Raises: + VimbaFeatureError if val is of type int or str and does not match an EnumEntry. + VimbaFeatureError if access rights are not sufficient. + VimbaFeatureError if executed within a registered change_handler. + """ + if type(val) in (EnumEntry, str): + as_entry = self.get_entry(str(val)) + + else: + as_entry = self.get_entry(int(val)) + + try: + call_vimba_c('VmbFeatureEnumSet', self._handle, self._info.name, bytes(as_entry)) + + except VimbaCError as e: + err = e.get_error_code() + + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + elif err == VmbError.InvalidCall: + exc = self._build_within_callback_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + +@TraceEnable() +def _discover_enum_entries(handle: VmbHandle, feat_name: str) -> EnumEntryTuple: + result = [] + enums_count = VmbUint32(0) + + call_vimba_c('VmbFeatureEnumRangeQuery', handle, feat_name, None, 0, byref(enums_count)) + + if enums_count.value: + enums_found = VmbUint32(0) + enums_names = (ctypes.c_char_p * enums_count.value)() + + call_vimba_c('VmbFeatureEnumRangeQuery', handle, feat_name, enums_names, enums_count, + byref(enums_found)) + + for enum_name in enums_names[:enums_found.value]: + enum_info = VmbFeatureEnumEntry() + + call_vimba_c('VmbFeatureEnumEntryGet', handle, feat_name, enum_name, byref(enum_info), + sizeof(VmbFeatureEnumEntry)) + + result.append(EnumEntry(handle, feat_name, enum_info)) + + return tuple(result) + + +class FloatFeature(_BaseFeature): + """The FloatFeature is a feature represented by a floating number.""" + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera, or Interface Types.""" + super().__init__(handle, info) + + def __str__(self): + try: + msg = 'FloatFeature(name={}, value={}, range={}, increment={})' + return msg.format(self.get_name(), self.get(), self.get_range(), self.get_increment()) + + except Exception: + return 'FloatFeature(name={})'.format(self.get_name()) + + @TraceEnable() + def get(self) -> float: + """Get current value (float). + + Returns: + Current float value. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_val = VmbDouble(0.0) + + try: + call_vimba_c('VmbFeatureFloatGet', self._handle, self._info.name, byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value + + @TraceEnable() + def get_range(self) -> Tuple[float, float]: + """Get range of accepted values + + Returns: + A pair of range boundaries. First value is the minimum, second value is the maximum. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_min = VmbDouble(0.0) + c_max = VmbDouble(0.0) + + try: + call_vimba_c('VmbFeatureFloatRangeQuery', self._handle, self._info.name, byref(c_min), + byref(c_max)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return (c_min.value, c_max.value) + + @TraceEnable() + def get_increment(self) -> Optional[float]: + """Get increment (steps between valid values, starting from minimum value). + + Returns: + The increment or None if the feature currently has no increment. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_has_val = VmbBool(False) + c_val = VmbDouble(False) + + try: + call_vimba_c('VmbFeatureFloatIncrementQuery', self._handle, self._info.name, + byref(c_has_val), byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value if c_has_val else None + + @TraceEnable() + def set(self, val: float): + """Set current value of type float. + + Arguments: + val - The float value to set. + + Raises: + VimbaFeatureError if access rights are not sufficient. + VimbaFeatureError if value is out of bounds. + VimbaFeatureError if executed within a registered change_handler. + """ + as_float = float(val) + + try: + call_vimba_c('VmbFeatureFloatSet', self._handle, self._info.name, as_float) + + except VimbaCError as e: + err = e.get_error_code() + + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + elif err == VmbError.InvalidValue: + exc = self._build_value_error(as_float) + + elif err == VmbError.InvalidCall: + exc = self._build_within_callback_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + def _build_value_error(self, val: float) -> VimbaFeatureError: + caller_name = inspect.stack()[1][3] + min_, max_ = self.get_range() + + # Value Errors for float mean always out-of-bounds + msg = 'Called \'{}()\' of Feature \'{}\' with invalid value. {} is not within [{}, {}].' + msg = msg.format(caller_name, self.get_name(), val, min_, max_) + + return VimbaFeatureError(msg) + + +class IntFeature(_BaseFeature): + """The IntFeature is a feature represented by an integer.""" + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera, or Interface Types.""" + super().__init__(handle, info) + + def __str__(self): + try: + msg = 'IntFeature(name={}, value={}, range={}, increment={})' + return msg.format(self.get_name(), self.get(), self.get_range(), self.get_increment()) + + except Exception: + return 'IntFeature(name={})'.format(self.get_name()) + + @TraceEnable() + def get(self) -> int: + """Get current value (int). + + Returns: + Current int value. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_val = VmbInt64() + + try: + call_vimba_c('VmbFeatureIntGet', self._handle, self._info.name, byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value + + @TraceEnable() + def get_range(self) -> Tuple[int, int]: + """Get range of accepted values. + + Returns: + A pair of range boundaries. First value is the minimum, second value is the maximum. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_min = VmbInt64() + c_max = VmbInt64() + + try: + call_vimba_c('VmbFeatureIntRangeQuery', self._handle, self._info.name, byref(c_min), + byref(c_max)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return (c_min.value, c_max.value) + + @TraceEnable() + def get_increment(self) -> int: + """Get increment (steps between valid values, starting from minimal values). + + Returns: + The increment of this feature. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_val = VmbInt64() + + try: + call_vimba_c('VmbFeatureIntIncrementQuery', self._handle, self._info.name, byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value + + @TraceEnable() + def set(self, val: int): + """Set current value of type int. + + Arguments: + val - The int value to set. + + Raises: + VimbaFeatureError if access rights are not sufficient. + VimbaFeatureError if value is out of bounds or misaligned to the increment. + VimbaFeatureError if executed within a registered change_handler. + """ + as_int = int(val) + + try: + call_vimba_c('VmbFeatureIntSet', self._handle, self._info.name, as_int) + + except VimbaCError as e: + err = e.get_error_code() + + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + elif err == VmbError.InvalidValue: + exc = self._build_value_error(as_int) + + elif err == VmbError.InvalidCall: + exc = self._build_within_callback_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + def _build_value_error(self, val) -> VimbaFeatureError: + caller_name = inspect.stack()[1][3] + min_, max_ = self.get_range() + + msg = 'Called \'{}()\' of Feature \'{}\' with invalid value. ' + + # Value out of bounds + if (val < min_) or (max_ < val): + msg += '{} is not within [{}, {}].'.format(val, min_, max_) + + # Misaligned value + else: + inc = self.get_increment() + msg += '{} is not a multiple of {}, starting at {}'.format(val, inc, min_) + + return VimbaFeatureError(msg.format(caller_name, self.get_name())) + + +class RawFeature(_BaseFeature): + """The RawFeature is a feature represented by sequence of bytes.""" + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera, or Interface Types.""" + super().__init__(handle, info) + + def __str__(self): + try: + msg = 'RawFeature(name={}, value={}, length={})' + return msg.format(self.get_name(), self.get(), self.length()) + + except Exception: + return 'RawFeature(name={})'.format(self.get_name()) + + @TraceEnable() + def get(self) -> bytes: # coverage: skip + """Get current value as a sequence of bytes + + Returns: + Current value. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + # Note: Coverage is skipped. RawFeature is not testable in a generic way + c_buf_avail = VmbUint32() + c_buf_len = self.length() + c_buf = create_string_buffer(c_buf_len) + + try: + call_vimba_c('VmbFeatureRawGet', self._handle, self._info.name, c_buf, c_buf_len, + byref(c_buf_avail)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_buf.raw[:c_buf_avail.value] + + @TraceEnable() + def set(self, buf: bytes): # coverage: skip + """Set current value as a sequence of bytes. + + Arguments: + val - The value to set. + + Raises: + VimbaFeatureError if access rights are not sufficient. + VimbaFeatureError if executed within a registered change_handler. + """ + # Note: Coverage is skipped. RawFeature is not testable in a generic way + as_bytes = bytes(buf) + + try: + call_vimba_c('VmbFeatureRawSet', self._handle, self._info.name, as_bytes, len(as_bytes)) + + except VimbaCError as e: + err = e.get_error_code() + + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + elif err == VmbError.InvalidCall: + exc = self._build_within_callback_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + @TraceEnable() + def length(self) -> int: # coverage: skip + """Get length of byte sequence representing the value. + + Returns: + Length of current value. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + # Note: Coverage is skipped. RawFeature is not testable in a generic way + c_val = VmbUint32() + + try: + call_vimba_c('VmbFeatureRawLengthQuery', self._handle, self._info.name, + byref(c_val)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_val.value + + +class StringFeature(_BaseFeature): + """The StringFeature is a feature represented by a string.""" + + @TraceEnable() + def __init__(self, handle: VmbHandle, info: VmbFeatureInfo): + """Do not call directly. Instead, access Features via System, Camera or Interface Types.""" + super().__init__(handle, info) + + def __str__(self): + try: + msg = 'StringFeature(name={}, value={}, max_length={})' + return msg.format(self.get_name(), self.get(), self.get_max_length()) + + except Exception: + return 'StringFeature(name={})'.format(self.get_name()) + + @TraceEnable() + def get(self) -> str: + """Get current value (str) + + Returns: + Current str value. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_buf_len = VmbUint32(0) + + # Query buffer length + try: + call_vimba_c('VmbFeatureStringGet', self._handle, self._info.name, None, 0, + byref(c_buf_len)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + c_buf = create_string_buffer(c_buf_len.value) + + # Copy string from C-Layer + try: + call_vimba_c('VmbFeatureStringGet', self._handle, self._info.name, c_buf, c_buf_len, + None) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_buf.value.decode() + + @TraceEnable() + def set(self, val: str): + """Set current value of type str. + + Arguments: + val - The str value to set. + + Raises: + VimbaFeatureError if access rights are not sufficient. + VimbaFeatureError if val exceeds the maximum string length. + VimbaFeatureError if executed within a registered change_handler. + """ + as_str = str(val) + + try: + call_vimba_c('VmbFeatureStringSet', self._handle, self._info.name, + as_str.encode('utf8')) + + except VimbaCError as e: + err = e.get_error_code() + + if err == VmbError.InvalidAccess: + exc = self._build_access_error() + + elif err == VmbError.InvalidValue: + exc = self.__build_value_error(as_str) + + elif err == VmbError.InvalidCall: + exc = self._build_within_callback_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + @TraceEnable() + def get_max_length(self) -> int: + """Get maximum string length the Feature can store. + + In this context, string length does not mean the number of characters, it means + the number of bytes after encoding. A string encoded in UTF-8 could exceed + the maximum length. + + Returns: + The number of ASCII characters the Feature can store. + + Raises: + VimbaFeatureError if access rights are not sufficient. + """ + c_max_len = VmbUint32(0) + + try: + call_vimba_c('VmbFeatureStringMaxlengthQuery', self._handle, self._info.name, + byref(c_max_len)) + + except VimbaCError as e: + if e.get_error_code() == VmbError.InvalidAccess: + exc = self._build_access_error() + + else: + exc = self._build_unhandled_error(e) + + raise exc from e + + return c_max_len.value + + def __build_value_error(self, val: str) -> VimbaFeatureError: + caller_name = inspect.stack()[1][3] + val_as_bytes = val.encode('utf8') + max_len = self.get_max_length() + + msg = 'Called \'{}()\' of Feature \'{}\' with invalid value. \'{}\' > max length \'{}\'.' + + return VimbaFeatureError(msg.format(caller_name, self.get_name(), val_as_bytes, max_len)) + + +FeatureTypes = Union[IntFeature, FloatFeature, StringFeature, BoolFeature, EnumFeature, + CommandFeature, RawFeature] + +FeatureTypeTypes = Union[Type[IntFeature], Type[FloatFeature], Type[StringFeature], + Type[BoolFeature], Type[EnumFeature], Type[CommandFeature], + Type[RawFeature]] + +FeaturesTuple = Tuple[FeatureTypes, ...] + + +def _build_feature(handle: VmbHandle, info: VmbFeatureInfo) -> FeatureTypes: + feat_value = VmbFeatureData(info.featureDataType) + + if VmbFeatureData.Int == feat_value: + feat = IntFeature(handle, info) + + elif VmbFeatureData.Float == feat_value: + feat = FloatFeature(handle, info) + + elif VmbFeatureData.String == feat_value: + feat = StringFeature(handle, info) + + elif VmbFeatureData.Bool == feat_value: + feat = BoolFeature(handle, info) + + elif VmbFeatureData.Enum == feat_value: + feat = EnumFeature(handle, info) + + elif VmbFeatureData.Command == feat_value: + feat = CommandFeature(handle, info) + + else: + feat = RawFeature(handle, info) + + return feat + + +@TraceEnable() +def discover_features(handle: VmbHandle) -> FeaturesTuple: + """Discover all features associated with the given handle. + + Arguments: + handle - Vimba entity used to find the associated features. + + Returns: + A set of all discovered Features associated with handle. + """ + result = [] + + feats_count = VmbUint32(0) + + call_vimba_c('VmbFeaturesList', handle, None, 0, byref(feats_count), sizeof(VmbFeatureInfo)) + + if feats_count: + feats_found = VmbUint32(0) + feats_infos = (VmbFeatureInfo * feats_count.value)() + + call_vimba_c('VmbFeaturesList', handle, feats_infos, feats_count, byref(feats_found), + sizeof(VmbFeatureInfo)) + + for info in feats_infos[:feats_found.value]: + result.append(_build_feature(handle, info)) + + return tuple(result) + + +@TraceEnable() +def discover_feature(handle: VmbHandle, feat_name: str) -> FeatureTypes: + """Discover a singe feature associated with the given handle. + + Arguments: + handle - Vimba entity used to find the associated feature. + feat_name: - Name of the Feature that should be searched. + + Returns: + The Feature associated with 'handle' by the name of 'feat_name' + """ + info = VmbFeatureInfo() + + call_vimba_c('VmbFeatureInfoQuery', handle, feat_name.encode('utf-8'), byref(info), + sizeof(VmbFeatureInfo)) + + return _build_feature(handle, info) diff --git a/copylot/hardware/cameras/avt/vimba/frame.py b/copylot/hardware/cameras/avt/vimba/frame.py new file mode 100644 index 00000000..0a267cb7 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/frame.py @@ -0,0 +1,923 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import enum +import ctypes +import copy +import functools + +from typing import Optional, Tuple +from .c_binding import byref, sizeof, decode_flags +from .c_binding import call_vimba_c, call_vimba_image_transform, VmbFrameStatus, VmbFrameFlags, \ + VmbFrame, VmbHandle, VmbPixelFormat, VmbImage, VmbDebayerMode, \ + VmbTransformInfo, PIXEL_FORMAT_CONVERTIBILITY_MAP, PIXEL_FORMAT_TO_LAYOUT +from .feature import FeaturesTuple, FeatureTypes, FeatureTypeTypes, discover_features +from .shared import filter_features_by_name, filter_features_by_type, filter_features_by_category, \ + attach_feature_accessors, remove_feature_accessors +from .util import TraceEnable, RuntimeTypeCheckEnable, EnterContextOnCall, LeaveContextOnCall, \ + RaiseIfOutsideContext +from .error import VimbaFrameError, VimbaFeatureError + +try: + import numpy # type: ignore + +except ModuleNotFoundError: + numpy = None # type: ignore + + +__all__ = [ + 'PixelFormat', + 'MONO_PIXEL_FORMATS', + 'BAYER_PIXEL_FORMATS', + 'RGB_PIXEL_FORMATS', + 'RGBA_PIXEL_FORMATS', + 'BGR_PIXEL_FORMATS', + 'BGRA_PIXEL_FORMATS', + 'YUV_PIXEL_FORMATS', + 'YCBCR_PIXEL_FORMATS', + 'COLOR_PIXEL_FORMATS', + 'OPENCV_PIXEL_FORMATS', + 'FrameStatus', + 'Debayer', + 'Frame', + 'FrameTuple', + 'FormatTuple', + 'intersect_pixel_formats' +] + + +# Forward declarations +FrameTuple = Tuple['Frame', ...] +FormatTuple = Tuple['PixelFormat', ...] + + +class PixelFormat(enum.IntEnum): + """Enum specifying all PixelFormats. Note: Not all Cameras support all Pixelformats. + + Mono formats: + Mono8 - Monochrome, 8 bits (PFNC:Mono8) + Mono10 - Monochrome, 10 bits in 16 bits (PFNC:Mono10) + Mono10p - Monochrome, 4x10 bits continuously packed in 40 bits + (PFNC:Mono10p) + Mono12 - Monochrome, 12 bits in 16 bits (PFNC:Mono12) + Mono12Packed - Monochrome, 2x12 bits in 24 bits (GEV:Mono12Packed) + Mono12p - Monochrome, 2x12 bits continuously packed in 24 bits + (PFNC:Mono12p) + Mono14 - Monochrome, 14 bits in 16 bits (PFNC:Mono14) + Mono16 - Monochrome, 16 bits (PFNC:Mono16) + + Bayer formats: + BayerGR8 - Bayer-color, 8 bits, starting with GR line + (PFNC:BayerGR8) + BayerRG8 - Bayer-color, 8 bits, starting with RG line + (PFNC:BayerRG8) + BayerGB8 - Bayer-color, 8 bits, starting with GB line + (PFNC:BayerGB8) + BayerBG8 - Bayer-color, 8 bits, starting with BG line + (PFNC:BayerBG8) + BayerGR10 - Bayer-color, 10 bits in 16 bits, starting with GR + line (PFNC:BayerGR10) + BayerRG10 - Bayer-color, 10 bits in 16 bits, starting with RG + line (PFNC:BayerRG10) + BayerGB10 - Bayer-color, 10 bits in 16 bits, starting with GB + line (PFNC:BayerGB10) + BayerBG10 - Bayer-color, 10 bits in 16 bits, starting with BG + line (PFNC:BayerBG10) + BayerGR12 - Bayer-color, 12 bits in 16 bits, starting with GR + line (PFNC:BayerGR12) + BayerRG12 - Bayer-color, 12 bits in 16 bits, starting with RG + line (PFNC:BayerRG12) + BayerGB12 - Bayer-color, 12 bits in 16 bits, starting with GB + line (PFNC:BayerGB12) + BayerBG12 - Bayer-color, 12 bits in 16 bits, starting with BG + line (PFNC:BayerBG12) + BayerGR12Packed - Bayer-color, 2x12 bits in 24 bits, starting with GR + line (GEV:BayerGR12Packed) + BayerRG12Packed - Bayer-color, 2x12 bits in 24 bits, starting with RG + line (GEV:BayerRG12Packed) + BayerGB12Packed - Bayer-color, 2x12 bits in 24 bits, starting with GB + line (GEV:BayerGB12Packed) + BayerBG12Packed - Bayer-color, 2x12 bits in 24 bits, starting with BG + line (GEV:BayerBG12Packed) + BayerGR10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with GR line (PFNC:BayerGR10p) + BayerRG10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with RG line (PFNC:BayerRG10p) + BayerGB10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with GB line (PFNC:BayerGB10p) + BayerBG10p - Bayer-color, 4x10 bits continuously packed in 40 + bits, starting with BG line (PFNC:BayerBG10p) + BayerGR12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with GR line (PFNC:BayerGR12p) + BayerRG12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with RG line (PFNC:BayerRG12p) + BayerGB12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with GB line (PFNC:BayerGB12p) + BayerBG12p - Bayer-color, 2x12 bits continuously packed in 24 + bits, starting with BG line (PFNC:BayerBG12p) + BayerGR16 - Bayer-color, 16 bits, starting with GR line + (PFNC:BayerGR16) + BayerRG16 - Bayer-color, 16 bits, starting with RG line + (PFNC:BayerRG16) + BayerGB16 - Bayer-color, 16 bits, starting with GB line + (PFNC:BayerGB16) + BayerBG16 - Bayer-color, 16 bits, starting with BG line + (PFNC:BayerBG16) + + RGB formats: + Rgb8 - RGB, 8 bits x 3 (PFNC:RGB8) + Bgr8 - BGR, 8 bits x 3 (PFNC:Bgr8) + Rgb10 - RGB, 10 bits in 16 bits x 3 (PFNC:RGB10) + Bgr10 - BGR, 10 bits in 16 bits x 3 (PFNC:BGR10) + Rgb12 - RGB, 12 bits in 16 bits x 3 (PFNC:RGB12) + Bgr12 - BGR, 12 bits in 16 bits x 3 (PFNC:BGR12) + Rgb14 - RGB, 14 bits in 16 bits x 3 (PFNC:RGB14) + Bgr14 - BGR, 14 bits in 16 bits x 3 (PFNC:BGR14) + Rgb16 - RGB, 16 bits x 3 (PFNC:RGB16) + Bgr16 - BGR, 16 bits x 3 (PFNC:BGR16) + + RGBA formats: + Argb8 - ARGB, 8 bits x 4 (PFNC:RGBa8) + Rgba8 - RGBA, 8 bits x 4, legacy name + Bgra8 - BGRA, 8 bits x 4 (PFNC:BGRa8) + Rgba10 - RGBA, 10 bits in 16 bits x 4 + Bgra10 - BGRA, 10 bits in 16 bits x 4 + Rgba12 - RGBA, 12 bits in 16 bits x 4 + Bgra12 - BGRA, 12 bits in 16 bits x 4 + Rgba14 - RGBA, 14 bits in 16 bits x 4 + Bgra14 - BGRA, 14 bits in 16 bits x 4 + Rgba16 - RGBA, 16 bits x 4 + Bgra16 - BGRA, 16 bits x 4 + + YUV/YCbCr formats: + Yuv411 - YUV 411 with 8 bits (GEV:YUV411Packed) + Yuv422 - YUV 422 with 8 bits (GEV:YUV422Packed) + Yuv444 - YUV 444 with 8 bits (GEV:YUV444Packed) + YCbCr411_8_CbYYCrYY - Y´CbCr 411 with 8 bits + (PFNC:YCbCr411_8_CbYYCrYY) - identical to Yuv411 + YCbCr422_8_CbYCrY - Y´CbCr 422 with 8 bits + (PFNC:YCbCr422_8_CbYCrY) - identical to Yuv422 + YCbCr8_CbYCr - Y´CbCr 444 with 8 bits + (PFNC:YCbCr8_CbYCr) - identical to Yuv444 + """ + # Mono Formats + Mono8 = VmbPixelFormat.Mono8 + Mono10 = VmbPixelFormat.Mono10 + Mono10p = VmbPixelFormat.Mono10p + Mono12 = VmbPixelFormat.Mono12 + Mono12Packed = VmbPixelFormat.Mono12Packed + Mono12p = VmbPixelFormat.Mono12p + Mono14 = VmbPixelFormat.Mono14 + Mono16 = VmbPixelFormat.Mono16 + + # Bayer Formats + BayerGR8 = VmbPixelFormat.BayerGR8 + BayerRG8 = VmbPixelFormat.BayerRG8 + BayerGB8 = VmbPixelFormat.BayerGB8 + BayerBG8 = VmbPixelFormat.BayerBG8 + BayerGR10 = VmbPixelFormat.BayerGR10 + BayerRG10 = VmbPixelFormat.BayerRG10 + BayerGB10 = VmbPixelFormat.BayerGB10 + BayerBG10 = VmbPixelFormat.BayerBG10 + BayerGR12 = VmbPixelFormat.BayerGR12 + BayerRG12 = VmbPixelFormat.BayerRG12 + BayerGB12 = VmbPixelFormat.BayerGB12 + BayerBG12 = VmbPixelFormat.BayerBG12 + BayerGR12Packed = VmbPixelFormat.BayerGR12Packed + BayerRG12Packed = VmbPixelFormat.BayerRG12Packed + BayerGB12Packed = VmbPixelFormat.BayerGB12Packed + BayerBG12Packed = VmbPixelFormat.BayerBG12Packed + BayerGR10p = VmbPixelFormat.BayerGR10p + BayerRG10p = VmbPixelFormat.BayerRG10p + BayerGB10p = VmbPixelFormat.BayerGB10p + BayerBG10p = VmbPixelFormat.BayerBG10p + BayerGR12p = VmbPixelFormat.BayerGR12p + BayerRG12p = VmbPixelFormat.BayerRG12p + BayerGB12p = VmbPixelFormat.BayerGB12p + BayerBG12p = VmbPixelFormat.BayerBG12p + BayerGR16 = VmbPixelFormat.BayerGR16 + BayerRG16 = VmbPixelFormat.BayerRG16 + BayerGB16 = VmbPixelFormat.BayerGB16 + BayerBG16 = VmbPixelFormat.BayerBG16 + + # RGB Formats + Rgb8 = VmbPixelFormat.Rgb8 + Bgr8 = VmbPixelFormat.Bgr8 + Rgb10 = VmbPixelFormat.Rgb10 + Bgr10 = VmbPixelFormat.Bgr10 + Rgb12 = VmbPixelFormat.Rgb12 + Bgr12 = VmbPixelFormat.Bgr12 + Rgb14 = VmbPixelFormat.Rgb14 + Bgr14 = VmbPixelFormat.Bgr14 + Rgb16 = VmbPixelFormat.Rgb16 + Bgr16 = VmbPixelFormat.Bgr16 + + # RGBA Formats + Rgba8 = VmbPixelFormat.Rgba8 + Bgra8 = VmbPixelFormat.Bgra8 + Argb8 = VmbPixelFormat.Argb8 + Rgba10 = VmbPixelFormat.Rgba10 + Bgra10 = VmbPixelFormat.Bgra10 + Rgba12 = VmbPixelFormat.Rgba12 + Bgra12 = VmbPixelFormat.Bgra12 + Rgba14 = VmbPixelFormat.Rgba14 + Bgra14 = VmbPixelFormat.Bgra14 + Rgba16 = VmbPixelFormat.Rgba16 + Bgra16 = VmbPixelFormat.Bgra16 + Yuv411 = VmbPixelFormat.Yuv411 + Yuv422 = VmbPixelFormat.Yuv422 + Yuv444 = VmbPixelFormat.Yuv444 + + # YCbCr Formats + YCbCr411_8_CbYYCrYY = VmbPixelFormat.YCbCr411_8_CbYYCrYY + YCbCr422_8_CbYCrY = VmbPixelFormat.YCbCr422_8_CbYCrY + YCbCr8_CbYCr = VmbPixelFormat.YCbCr8_CbYCr + + def __str__(self): + return self._name_ + + def __repr__(self): + return 'PixelFormat.{}'.format(str(self)) + + def get_convertible_formats(self) -> Tuple['PixelFormat', ...]: + formats = PIXEL_FORMAT_CONVERTIBILITY_MAP[VmbPixelFormat(self)] + return tuple([PixelFormat(fmt) for fmt in formats]) + + +MONO_PIXEL_FORMATS = ( + PixelFormat.Mono8, + PixelFormat.Mono10, + PixelFormat.Mono10p, + PixelFormat.Mono12, + PixelFormat.Mono12Packed, + PixelFormat.Mono12p, + PixelFormat.Mono14, + PixelFormat.Mono16 +) + + +BAYER_PIXEL_FORMATS = ( + PixelFormat.BayerGR8, + PixelFormat.BayerRG8, + PixelFormat.BayerGB8, + PixelFormat.BayerBG8, + PixelFormat.BayerGR10, + PixelFormat.BayerRG10, + PixelFormat.BayerGB10, + PixelFormat.BayerBG10, + PixelFormat.BayerGR12, + PixelFormat.BayerRG12, + PixelFormat.BayerGB12, + PixelFormat.BayerBG12, + PixelFormat.BayerGR12Packed, + PixelFormat.BayerRG12Packed, + PixelFormat.BayerGB12Packed, + PixelFormat.BayerBG12Packed, + PixelFormat.BayerGR10p, + PixelFormat.BayerRG10p, + PixelFormat.BayerGB10p, + PixelFormat.BayerBG10p, + PixelFormat.BayerGR12p, + PixelFormat.BayerRG12p, + PixelFormat.BayerGB12p, + PixelFormat.BayerBG12p, + PixelFormat.BayerGR16, + PixelFormat.BayerRG16, + PixelFormat.BayerGB16, + PixelFormat.BayerBG16 +) + + +RGB_PIXEL_FORMATS = ( + PixelFormat.Rgb8, + PixelFormat.Rgb10, + PixelFormat.Rgb12, + PixelFormat.Rgb14, + PixelFormat.Rgb16 +) + + +RGBA_PIXEL_FORMATS = ( + PixelFormat.Rgba8, + PixelFormat.Argb8, + PixelFormat.Rgba10, + PixelFormat.Rgba12, + PixelFormat.Rgba14, + PixelFormat.Rgba16 +) + + +BGR_PIXEL_FORMATS = ( + PixelFormat.Bgr8, + PixelFormat.Bgr10, + PixelFormat.Bgr12, + PixelFormat.Bgr14, + PixelFormat.Bgr16 +) + + +BGRA_PIXEL_FORMATS = ( + PixelFormat.Bgra8, + PixelFormat.Bgra10, + PixelFormat.Bgra12, + PixelFormat.Bgra14, + PixelFormat.Bgra16 +) + + +YUV_PIXEL_FORMATS = ( + PixelFormat.Yuv411, + PixelFormat.Yuv422, + PixelFormat.Yuv444 +) + + +YCBCR_PIXEL_FORMATS = ( + PixelFormat.YCbCr411_8_CbYYCrYY, + PixelFormat.YCbCr422_8_CbYCrY, + PixelFormat.YCbCr8_CbYCr +) + + +COLOR_PIXEL_FORMATS = BAYER_PIXEL_FORMATS + RGB_PIXEL_FORMATS + RGBA_PIXEL_FORMATS + \ + BGR_PIXEL_FORMATS + BGRA_PIXEL_FORMATS + YUV_PIXEL_FORMATS + \ + YCBCR_PIXEL_FORMATS + + +OPENCV_PIXEL_FORMATS = ( + PixelFormat.Mono8, + PixelFormat.Bgr8, + PixelFormat.Bgra8, + PixelFormat.Mono16, + PixelFormat.Bgr16, + PixelFormat.Bgra16 +) + + +class Debayer(enum.IntEnum): + """Enum specifying debayer modes. + + Enum values: + Mode2x2 - 2x2 with green averaging (this is the default if no debayering algorithm + is added as transformation option). + Mode3x3 - 3x3 with equal green weighting per line (8-bit images only). + ModeLCAA - Debayering with horizontal local color anti-aliasing (8-bit images only). + ModeLCAAV - Debayering with horizontal and vertical local color anti-aliasing + ( 8-bit images only). + ModeYuv422 - Debayering with YUV422-alike sub-sampling (8-bit images only). + """ + Mode2x2 = VmbDebayerMode.Mode_2x2 + Mode3x3 = VmbDebayerMode.Mode_3x3 + ModeLCAA = VmbDebayerMode.Mode_LCAA + ModeLCAAV = VmbDebayerMode.Mode_LCAAV + ModeYuv422 = VmbDebayerMode.Mode_YUV422 + + def __str__(self): + return 'DebayerMode.{}'.format(self._name_) + + def __repr__(self): + return str(self) + + +class FrameStatus(enum.IntEnum): + """Enum specifying the current status of internal Frame data. + + Enum values: + Complete - Frame data is complete without errors. + Incomplete - Frame could not be filled to the end. + TooSmall - Frame buffer was too small. + Invalid - Frame buffer was invalid. + """ + + Complete = VmbFrameStatus.Complete + Incomplete = VmbFrameStatus.Incomplete + TooSmall = VmbFrameStatus.TooSmall + Invalid = VmbFrameStatus.Invalid + + +class AllocationMode(enum.IntEnum): + """Enum specifying the supported frame allocation modes. + + Enum values: + AnnounceFrame - The buffer is allocated by VimbaPython + AllocAndAnnounceFrame - The buffer is allocated by the Transport Layer + """ + AnnounceFrame = 0 + AllocAndAnnounceFrame = 1 + + +class AncillaryData: + """Ancillary Data are created after enabling a Cameras 'ChunkModeActive' Feature. + Ancillary Data are Features stored within a Frame. + """ + @TraceEnable() + @LeaveContextOnCall() + def __init__(self, handle: VmbFrame): + """Do not call directly. Get Object via Frame access method""" + self.__handle: VmbFrame = handle + self.__data_handle: VmbHandle = VmbHandle() + self.__feats: FeaturesTuple = () + self.__context_cnt: int = 0 + + @TraceEnable() + def __enter__(self): + if not self.__context_cnt: + self._open() + + self.__context_cnt += 1 + return self + + @TraceEnable() + def __exit__(self, exc_type, exc_value, exc_traceback): + self.__context_cnt -= 1 + + if not self.__context_cnt: + self._close() + + @RaiseIfOutsideContext() + def get_all_features(self) -> FeaturesTuple: + """Get all features in ancillary data. + + Returns: + A set of all currently features stored in Ancillary Data. + + Raises: + RuntimeError then called outside of "with" - statement. + """ + return self.__feats + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_type(self, feat_type: FeatureTypeTypes) -> FeaturesTuple: + """Get all features in ancillary data of a specific type. + + Valid FeatureTypes are: IntFeature, FloatFeature, StringFeature, BoolFeature, + EnumFeature, CommandFeature, RawFeature + + Arguments: + feat_type - FeatureType used find features of that type. + + Returns: + A all features of type 'feat_type'. + + Raises: + RuntimeError then called outside of "with" - statement. + TypeError if parameters do not match their type hint. + """ + return filter_features_by_type(self.__feats, feat_type) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_category(self, category: str) -> FeaturesTuple: + """Get all features in ancillary data of a specific category. + + Arguments: + category - Category that should be used for filtering. + + Returns: + A all features of category 'category'. + + Raises: + RuntimeError then called outside of "with" - statement. + TypeError if parameters do not match their type hint. + """ + return filter_features_by_category(self.__feats, category) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_feature_by_name(self, feat_name: str) -> FeatureTypes: + """Get a features in ancillary data by its name. + + Arguments: + feat_name - Name used to find a feature. + + Returns: + Feature with the associated name. + + Raises: + RuntimeError then called outside of "with" - statement. + TypeError if parameters do not match their type hint. + VimbaFeatureError if no feature is associated with 'feat_name'. + """ + feat = filter_features_by_name(self.__feats, feat_name) + + if not feat: + raise VimbaFeatureError('Feature \'{}\' not found.'.format(feat_name)) + + return feat + + @TraceEnable() + @EnterContextOnCall() + def _open(self): + call_vimba_c('VmbAncillaryDataOpen', byref(self.__handle), byref(self.__data_handle)) + + self.__feats = _replace_invalid_feature_calls(discover_features(self.__data_handle)) + attach_feature_accessors(self, self.__feats) + + @TraceEnable() + @LeaveContextOnCall() + def _close(self): + remove_feature_accessors(self, self.__feats) + self.__feats = () + + call_vimba_c('VmbAncillaryDataClose', self.__data_handle) + self.__data_handle = VmbHandle() + + +def _replace_invalid_feature_calls(feats: FeaturesTuple) -> FeaturesTuple: + # AncillaryData are basically "lightweight" features. Calling most feature related + # Functions with a AncillaryData - Handle leads to VimbaC Errors. This method decorates + # all Methods that are unsafe to call with a decorator raising a RuntimeError. + to_wrap = [ + 'get_access_mode', + 'is_readable', + 'is_writeable', + 'register_change_handler', + 'get_increment', + 'get_range', + 'set' + ] + + # Decorator raising a RuntimeError instead of delegating call to inner function. + def invalid_call(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + msg = 'Calling \'{}\' is invalid for AncillaryData Features.' + raise RuntimeError(msg.format(func.__name__)) + + return wrapper + + # Replace original implementation by injecting a surrounding decorator and + # binding the resulting function as a method to the Feature instance. + for f, a in [(f, a) for f in feats for a in to_wrap]: + try: + fn = invalid_call(getattr(f, a)) + setattr(f, a, fn.__get__(f)) + + except AttributeError: + pass + + return feats + + +class Frame: + """This class allows access to Frames acquired by a camera. The Frame is basically + a buffer that wraps image data and some metadata. + """ + def __init__(self, buffer_size: int, allocation_mode: AllocationMode): + """Do not call directly. Create Frames via Camera methods instead.""" + self._allocation_mode = allocation_mode + + # Allocation is not necessary for the AllocAndAnnounce case. In that case the Transport + # Layer will take care of buffer allocation. The self._buffer variable will be updated after + # the frame is announced and memory has been allocated. + if self._allocation_mode == AllocationMode.AnnounceFrame: + self._buffer = (ctypes.c_ubyte * buffer_size)() + self._frame: VmbFrame = VmbFrame() + + # Setup underlaying Frame + if self._allocation_mode == AllocationMode.AnnounceFrame: + self._frame.buffer = ctypes.cast(self._buffer, ctypes.c_void_p) + self._frame.bufferSize = sizeof(self._buffer) + elif self._allocation_mode == AllocationMode.AllocAndAnnounceFrame: + # Set buffer pointer to NULL and inform Transport Layer of size it should allocate + self._frame.buffer = None + self._frame.bufferSize = buffer_size + + def __str__(self): + msg = 'Frame(id={}, status={}, buffer={})' + return msg.format(self._frame.frameID, str(FrameStatus(self._frame.receiveStatus)), + hex(self._frame.buffer)) + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + + # VmbFrame contains Pointers and ctypes.Structure with Pointers can't be copied. + # As a workaround VmbFrame contains a deepcopy-like Method performing deep copy of all + # Attributes except PointerTypes. Those must be set manually after the copy operation. + setattr(result, '_buffer', copy.deepcopy(self._buffer, memo)) + setattr(result, '_frame', self._frame.deepcopy_skip_ptr(memo)) + + result._frame.buffer = ctypes.cast(result._buffer, ctypes.c_void_p) + result._frame.bufferSize = sizeof(result._buffer) + + return result + + def _set_buffer(self, buffer: ctypes.c_void_p): + """Set self._buffer to memory pointed to by passed buffer pointer + + Useful if frames were allocated with AllocationMode.AllocAndAnnounce + """ + self._buffer = ctypes.cast(buffer, + ctypes.POINTER(ctypes.c_ubyte * self._frame.bufferSize)).contents + + def get_buffer(self) -> ctypes.Array: + """Get internal buffer object containing image data.""" + return self._buffer + + def get_buffer_size(self) -> int: + """Get byte size of internal buffer.""" + return self._frame.bufferSize + + def get_image_size(self) -> int: + """Get byte size of image data stored in buffer.""" + return self._frame.imageSize + + def get_ancillary_data(self) -> Optional[AncillaryData]: + """Get AncillaryData. + + Frames acquired with cameras where Feature ChunkModeActive is enabled can contain + ancillary data within the image data. + + Returns: + None if Frame contains no ancillary data. + AncillaryData if Frame contains ancillary data. + """ + if not self._frame.ancillarySize: + return None + + return AncillaryData(self._frame) + + def get_status(self) -> FrameStatus: + """Returns current frame status.""" + return FrameStatus(self._frame.receiveStatus) + + def get_pixel_format(self) -> PixelFormat: + """Get format of the acquired image data""" + return PixelFormat(self._frame.pixelFormat) + + def get_height(self) -> Optional[int]: + """Get image height in pixels. + + Returns: + Image height in pixels if dimension data is provided by the camera. + None if dimension data is not provided by the camera. + """ + flags = decode_flags(VmbFrameFlags, self._frame.receiveFlags) + + if VmbFrameFlags.Dimension not in flags: + return None + + return self._frame.height + + def get_width(self) -> Optional[int]: + """Get image width in pixels. + + Returns: + Image width in pixels if dimension data is provided by the camera. + None if dimension data is not provided by the camera. + """ + flags = decode_flags(VmbFrameFlags, self._frame.receiveFlags) + + if VmbFrameFlags.Dimension not in flags: + return None + + return self._frame.width + + def get_offset_x(self) -> Optional[int]: + """Get horizontal offset in pixels. + + Returns: + Horizontal offset in pixel if offset data is provided by the camera. + None if offset data is not provided by the camera. + """ + flags = decode_flags(VmbFrameFlags, self._frame.receiveFlags) + + if VmbFrameFlags.Offset not in flags: + return None + + return self._frame.offsetX + + def get_offset_y(self) -> Optional[int]: + """Get vertical offset in pixels. + + Returns: + Vertical offset in pixels if offset data is provided by the camera. + None if offset data is not provided by the camera. + """ + flags = decode_flags(VmbFrameFlags, self._frame.receiveFlags) + + if VmbFrameFlags.Offset not in flags: + return None + + return self._frame.offsetY + + def get_id(self) -> Optional[int]: + """Get Frame ID. + + Returns: + Frame ID if the id is provided by the camera. + None if frame id is not provided by the camera. + """ + flags = decode_flags(VmbFrameFlags, self._frame.receiveFlags) + + if VmbFrameFlags.FrameID not in flags: + return None + + return self._frame.frameID + + def get_timestamp(self) -> Optional[int]: + """Get Frame timestamp. + + Returns: + Timestamp if provided by the camera. + None if timestamp is not provided by the camera. + """ + flags = decode_flags(VmbFrameFlags, self._frame.receiveFlags) + + if VmbFrameFlags.Timestamp not in flags: + return None + + return self._frame.timestamp + + @RuntimeTypeCheckEnable() + def convert_pixel_format(self, target_fmt: PixelFormat, + debayer_mode: Optional[Debayer] = None): + """Convert internal pixel format to given format. + + Note: This method allocates a new buffer for internal image data leading to some + runtime overhead. For performance reasons, it might be better to set the value + of the camera's 'PixelFormat' feature instead. In addition, a non-default debayer mode + can be specified. + + Arguments: + target_fmt - PixelFormat to convert to. + debayer_mode - Non-default algorithm used to debayer images in Bayer Formats. If + no mode is specified, default debayering mode 'Mode2x2' is applied. If + the current format is no Bayer format, this parameter is silently + ignored. + + Raises: + TypeError if parameters do not match their type hint. + ValueError if the current format can't be converted into 'target_fmt'. Convertible + Formats can be queried via get_convertible_formats() of PixelFormat. + AssertionError if image width or height can't be determined. + """ + + global BAYER_PIXEL_FORMATS + + # 1) Perform sanity checking + fmt = self.get_pixel_format() + + if fmt == target_fmt: + return + + if target_fmt not in fmt.get_convertible_formats(): + raise ValueError('Current PixelFormat can\'t be converted into given format.') + + # 2) Specify Transformation Input Image + height = self._frame.height + width = self._frame.width + + c_src_image = VmbImage() + c_src_image.Size = sizeof(c_src_image) + c_src_image.Data = ctypes.cast(self._buffer, ctypes.c_void_p) + + call_vimba_image_transform('VmbSetImageInfoFromPixelFormat', fmt, width, height, + byref(c_src_image)) + + # 3) Specify Transformation Output Image + c_dst_image = VmbImage() + c_dst_image.Size = sizeof(c_dst_image) + + layout, bits = PIXEL_FORMAT_TO_LAYOUT[VmbPixelFormat(target_fmt)] + + call_vimba_image_transform('VmbSetImageInfoFromInputImage', byref(c_src_image), layout, + bits, byref(c_dst_image)) + + # 4) Allocate Buffer and perform transformation + img_size = int(height * width * c_dst_image.ImageInfo.PixelInfo.BitsPerPixel / 8) + anc_size = self._frame.ancillarySize + + buf = (ctypes.c_ubyte * (img_size + anc_size))() + c_dst_image.Data = ctypes.cast(buf, ctypes.c_void_p) + + # 5) Setup Debayering mode if given. + transform_info = VmbTransformInfo() + if debayer_mode and (fmt in BAYER_PIXEL_FORMATS): + call_vimba_image_transform('VmbSetDebayerMode', VmbDebayerMode(debayer_mode), + byref(transform_info)) + + # 6) Perform Transformation + call_vimba_image_transform('VmbImageTransform', byref(c_src_image), byref(c_dst_image), + byref(transform_info), 1) + + # 7) Copy ancillary data if existing + if anc_size: + src = ctypes.addressof(self._buffer) + self._frame.imageSize + dst = ctypes.addressof(buf) + img_size + + ctypes.memmove(dst, src, anc_size) + + # 8) Update frame metadata + self._buffer = buf + self._frame.buffer = ctypes.cast(self._buffer, ctypes.c_void_p) + self._frame.bufferSize = sizeof(self._buffer) + self._frame.imageSize = img_size + self._frame.pixelFormat = target_fmt + + def as_numpy_ndarray(self) -> 'numpy.ndarray': + """Construct numpy.ndarray view on VimbaFrame. + + Returns: + numpy.ndarray on internal image buffer. + + Raises: + ImportError if numpy is not installed. + VimbaFrameError if current PixelFormat can't be converted to a numpy.ndarray. + """ + if numpy is None: + raise ImportError('\'Frame.as_opencv_image()\' requires module \'numpy\'.') + + # Construct numpy overlay on underlaying image buffer + height = self._frame.height + width = self._frame.width + fmt = self._frame.pixelFormat + + c_image = VmbImage() + c_image.Size = sizeof(c_image) + + call_vimba_image_transform('VmbSetImageInfoFromPixelFormat', fmt, width, height, + byref(c_image)) + + layout = PIXEL_FORMAT_TO_LAYOUT.get(fmt) + + if not layout: + msg = 'Can\'t construct numpy.ndarray for Pixelformat {}. ' \ + 'Use \'frame.convert_pixel_format()\' to convert to a different Pixelformat.' + raise VimbaFrameError(msg.format(str(self.get_pixel_format()))) + + bits_per_channel = layout[1] + channels_per_pixel = c_image.ImageInfo.PixelInfo.BitsPerPixel // bits_per_channel + + return numpy.ndarray(shape=(height, width, channels_per_pixel), + buffer=self._buffer, # type: ignore + dtype=numpy.uint8 if bits_per_channel == 8 else numpy.uint16) + + def as_opencv_image(self) -> 'numpy.ndarray': + """Construct OpenCV compatible view on VimbaFrame. + + Returns: + OpenCV compatible numpy.ndarray + + Raises: + ImportError if numpy is not installed. + ValueError if current pixel format is not compatible with opencv. Compatible + formats are in OPENCV_PIXEL_FORMATS. + """ + global OPENCV_PIXEL_FORMATS + + if numpy is None: + raise ImportError('\'Frame.as_opencv_image()\' requires module \'numpy\'.') + + fmt = self._frame.pixelFormat + + if fmt not in OPENCV_PIXEL_FORMATS: + raise ValueError('Current Format \'{}\' is not in OPENCV_PIXEL_FORMATS'.format( + str(PixelFormat(self._frame.pixelFormat)))) + + return self.as_numpy_ndarray() + + +@TraceEnable() +@RuntimeTypeCheckEnable() +def intersect_pixel_formats(fmts1: FormatTuple, fmts2: FormatTuple) -> FormatTuple: + """Build intersection of two sets containing PixelFormat. + + Arguments: + fmts1 - PixelFormats to intersect with fmts2 + fmts2 - PixelFormats to intersect with fmts1 + + Returns: + Set of PixelFormats that occur in fmts1 and fmts2 + + Raises: + TypeError if parameters do not match their type hint. + """ + return tuple(set(fmts1).intersection(set(fmts2))) diff --git a/copylot/hardware/cameras/avt/vimba/interface.py b/copylot/hardware/cameras/avt/vimba/interface.py new file mode 100644 index 00000000..840115d7 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/interface.py @@ -0,0 +1,391 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import enum +from typing import Tuple, List, Callable, Dict +from .c_binding import call_vimba_c, byref, sizeof, decode_cstr +from .c_binding import VmbInterface, VmbInterfaceInfo, VmbHandle, VmbUint32 +from .feature import discover_features, FeatureTypes, FeaturesTuple, FeatureTypeTypes +from .shared import filter_features_by_name, filter_features_by_type, filter_affected_features, \ + filter_selected_features, filter_features_by_category, \ + attach_feature_accessors, remove_feature_accessors, read_memory, \ + write_memory, read_registers, write_registers +from .util import TraceEnable, RuntimeTypeCheckEnable, EnterContextOnCall, LeaveContextOnCall, \ + RaiseIfOutsideContext +from .error import VimbaFeatureError + + +__all__ = [ + 'InterfaceType', + 'Interface', + 'InterfaceEvent', + 'InterfaceChangeHandler', + 'InterfacesTuple', + 'InterfacesList', + 'discover_interfaces', + 'discover_interface' +] + + +# Forward declarations +InterfaceChangeHandler = Callable[['Interface', 'InterfaceEvent'], None] +InterfacesTuple = Tuple['Interface', ...] +InterfacesList = List['Interface'] + + +class InterfaceType(enum.IntEnum): + """Enum specifying all interface types. + + Enum values: + Unknown - Interface is not known to this VimbaPython version. + Firewire - 1394 + Ethernet - Gigabit Ethernet + Usb - USB 3.0 + CL - Camera Link + CSI2 - CSI-2 + """ + Unknown = VmbInterface.Unknown + Firewire = VmbInterface.Firewire + Ethernet = VmbInterface.Ethernet + Usb = VmbInterface.Usb + CL = VmbInterface.CL + CSI2 = VmbInterface.CSI2 + + +class InterfaceEvent(enum.IntEnum): + """Enum specifying an Interface Event + + Enum values: + Missing - A known interface disappeared from the bus + Detected - A new interface was discovered + Reachable - A known interface can be accessed + Unreachable - A known interface cannot be accessed anymore + """ + Missing = 0 + Detected = 1 + Reachable = 2 + Unreachable = 3 + + +class Interface: + """This class allows access to an interface such as USB detected by Vimba. + Interface is meant to be used in conjunction with the "with" - statement. On entering a context, + all Interface features are detected and can be accessed within the context. Static Interface + properties like Name can be accessed outside the context. + """ + + @TraceEnable() + @LeaveContextOnCall() + def __init__(self, info: VmbInterfaceInfo): + """Do not call directly. Access Interfaces via vimba.Vimba instead.""" + self.__handle: VmbHandle = VmbHandle(0) + self.__info: VmbInterfaceInfo = info + self.__feats: FeaturesTuple = () + self.__context_cnt: int = 0 + + @TraceEnable() + def __enter__(self): + if not self.__context_cnt: + self._open() + + self.__context_cnt += 1 + return self + + @TraceEnable() + def __exit__(self, exc_type, exc_value, exc_traceback): + self.__context_cnt -= 1 + + if not self.__context_cnt: + self._close() + + def __str__(self): + return 'Interface(id={})'.format(self.get_id()) + + def __repr__(self): + rep = 'Interface' + rep += '(__handle=' + repr(self.__handle) + rep += ',__info=' + repr(self.__info) + rep += ')' + return rep + + def get_id(self) -> str: + """Get Interface Id such as VimbaUSBInterface_0x0.""" + return decode_cstr(self.__info.interfaceIdString) + + def get_type(self) -> InterfaceType: + """Get Interface Type such as InterfaceType.Usb.""" + return InterfaceType(self.__info.interfaceType) + + def get_name(self) -> str: + """Get Interface Name such as Vimba USB Interface.""" + return decode_cstr(self.__info.interfaceName) + + def get_serial(self) -> str: + """Get Interface Serial or '' if not set.""" + return decode_cstr(self.__info.serialString) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def read_memory(self, addr: int, max_bytes: int) -> bytes: # coverage: skip + """Read a byte sequence from a given memory address. + + Arguments: + addr: Starting address to read from. + max_bytes: Maximum number of bytes to read from addr. + + Returns: + Read memory contents as bytes. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + ValueError if addr is negative. + ValueError if max_bytes is negative. + ValueError if the memory access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return read_memory(self.__handle, addr, max_bytes) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def write_memory(self, addr: int, data: bytes): # coverage: skip + """Write a byte sequence to a given memory address. + + Arguments: + addr: Address to write the content of 'data' to. + data: Byte sequence to write at address 'addr'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + ValueError if addr is negative. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return write_memory(self.__handle, addr, data) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def read_registers(self, addrs: Tuple[int, ...]) -> Dict[int, int]: # coverage: skip + """Read contents of multiple registers. + + Arguments: + addrs: Sequence of addresses that should be read iteratively. + + Returns: + Dictionary containing a mapping from given address to the read register values. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + ValueError if any address in addrs is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return read_registers(self.__handle, addrs) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def write_registers(self, addrs_values: Dict[int, int]): # coverage: skip + """Write data to multiple registers. + + Arguments: + addrs_values: Mapping between register addresses and the data to write. + + Raises: + TypeError if parameters do not match their type hint. + ValueError if any address in addrs_values is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return write_registers(self.__handle, addrs_values) + + @RaiseIfOutsideContext() + def get_all_features(self) -> FeaturesTuple: + """Get access to all discovered features of this Interface. + + Returns: + A set of all currently detected features. + + Raises: + RuntimeError if called outside "with" - statement. + """ + return self.__feats + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_affected_by(self, feat: FeatureTypes) -> FeaturesTuple: + """Get all features affected by a specific interface feature. + + Arguments: + feat - Feature to find features that are affected by 'feat'. + + Returns: + A set of features affected by changes on 'feat'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + VimbaFeatureError if 'feat' is not a feature of this interface. + """ + return filter_affected_features(self.__feats, feat) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_selected_by(self, feat: FeatureTypes) -> FeaturesTuple: + """Get all features selected by a specific interface feature. + + Arguments: + feat - Feature to find features that are selected by 'feat'. + + Returns: + A set of features selected by changes on 'feat'. + + Raises: + TypeError if 'feat' is not of any feature type. + RuntimeError if called outside "with" - statement. + VimbaFeatureError if 'feat' is not a feature of this interface. + """ + return filter_selected_features(self.__feats, feat) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_type(self, feat_type: FeatureTypeTypes) -> FeaturesTuple: + """Get all interface features of a specific feature type. + + Valid FeatureTypes are: IntFeature, FloatFeature, StringFeature, BoolFeature, + EnumFeature, CommandFeature, RawFeature + + Arguments: + feat_type - FeatureType used find features of that type. + + Returns: + A set of features of type 'feat_type'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + """ + return filter_features_by_type(self.__feats, feat_type) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_category(self, category: str) -> FeaturesTuple: + """Get all interface features of a specific category. + + Arguments: + category - category for filtering. + + Returns: + A set of features of category 'category'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + """ + return filter_features_by_category(self.__feats, category) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_feature_by_name(self, feat_name: str) -> FeatureTypes: + """Get an interface feature by its name. + + Arguments: + feat_name - Name to find a feature. + + Returns: + Feature with the associated name. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called outside "with" - statement. + VimbaFeatureError if no feature is associated with 'feat_name'. + """ + feat = filter_features_by_name(self.__feats, feat_name) + + if not feat: + raise VimbaFeatureError('Feature \'{}\' not found.'.format(feat_name)) + + return feat + + @TraceEnable() + @EnterContextOnCall() + def _open(self): + call_vimba_c('VmbInterfaceOpen', self.__info.interfaceIdString, byref(self.__handle)) + + self.__feats = discover_features(self.__handle) + attach_feature_accessors(self, self.__feats) + + @TraceEnable() + @LeaveContextOnCall() + def _close(self): + for feat in self.__feats: + feat.unregister_all_change_handlers() + + remove_feature_accessors(self, self.__feats) + self.__feats = () + + call_vimba_c('VmbInterfaceClose', self.__handle) + + self.__handle = VmbHandle(0) + + +@TraceEnable() +def discover_interfaces() -> InterfacesList: + """Do not call directly. Access Interfaces via vimba.System instead.""" + + result = [] + inters_count = VmbUint32(0) + + call_vimba_c('VmbInterfacesList', None, 0, byref(inters_count), sizeof(VmbInterfaceInfo)) + + if inters_count: + inters_found = VmbUint32(0) + inters_infos = (VmbInterfaceInfo * inters_count.value)() + + call_vimba_c('VmbInterfacesList', inters_infos, inters_count, byref(inters_found), + sizeof(VmbInterfaceInfo)) + + for info in inters_infos[:inters_found.value]: + result.append(Interface(info)) + + return result + + +@TraceEnable() +def discover_interface(id_: str) -> Interface: + """Do not call directly. Access Interfaces via vimba.System instead.""" + + # Since there is no function to query a single interface, discover all interfaces and + # extract the Interface with the matching ID. + inters = discover_interfaces() + return [i for i in inters if id_ == i.get_id()].pop() diff --git a/copylot/hardware/cameras/avt/vimba/shared.py b/copylot/hardware/cameras/avt/vimba/shared.py new file mode 100644 index 00000000..fe0fa1f5 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/shared.py @@ -0,0 +1,357 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import itertools + +from typing import Dict, Tuple +from .c_binding import VmbUint32, VmbUint64, VmbHandle, VmbFeatureInfo +from .c_binding import call_vimba_c, byref, sizeof, create_string_buffer, VimbaCError +from .feature import FeaturesTuple, FeatureTypes, FeatureTypeTypes +from .error import VimbaFeatureError +from .util import TraceEnable + +__all__ = [ + 'filter_affected_features', + 'filter_selected_features', + 'filter_features_by_name', + 'filter_features_by_type', + 'filter_features_by_category', + 'attach_feature_accessors', + 'remove_feature_accessors', + 'read_memory', + 'write_memory', + 'read_registers', + 'write_registers' +] + + +@TraceEnable() +def filter_affected_features(feats: FeaturesTuple, feat: FeatureTypes) -> FeaturesTuple: + """Search for all Features affected by a given feature within a feature set. + + Arguments: + feats: Feature set to search in. + feat: Feature that might affect Features within 'feats'. + + Returns: + A set of all features that are affected by 'feat'. + + Raises: + VimbaFeatureError if 'feat' is not stored within 'feats'. + """ + + if feat not in feats: + raise VimbaFeatureError('Feature \'{}\' not in given Features'.format(feat.get_name())) + + result = [] + + if feat.has_affected_features(): + feats_count = VmbUint32() + feats_handle = feat._handle + feats_name = feat._info.name + + # Query affected features from given Feature + call_vimba_c('VmbFeatureListAffected', feats_handle, feats_name, None, 0, + byref(feats_count), sizeof(VmbFeatureInfo)) + + feats_found = VmbUint32(0) + feats_infos = (VmbFeatureInfo * feats_count.value)() + + call_vimba_c('VmbFeatureListAffected', feats_handle, feats_name, feats_infos, feats_count, + byref(feats_found), sizeof(VmbFeatureInfo)) + + # Search affected features in given feature set + for info, feature in itertools.product(feats_infos[:feats_found.value], feats): + if info.name == feature._info.name: + result.append(feature) + + return tuple(result) + + +@TraceEnable() +def filter_selected_features(feats: FeaturesTuple, feat: FeatureTypes) -> FeaturesTuple: + """Search for all Features selected by a given feature within a feature set. + + Arguments: + feats: Feature set to search in. + feat: Feature that might select Features within 'feats'. + + Returns: + A set of all features that are selected by 'feat'. + + Raises: + VimbaFeatureError if 'feat' is not stored within 'feats'. + """ + if feat not in feats: + raise VimbaFeatureError('Feature \'{}\' not in given Features'.format(feat.get_name())) + + result = [] + + if feat.has_selected_features(): + feats_count = VmbUint32() + feats_handle = feat._handle + feats_name = feat._info.name + + # Query selected features from given feature + call_vimba_c('VmbFeatureListSelected', feats_handle, feats_name, None, 0, + byref(feats_count), sizeof(VmbFeatureInfo)) + + feats_found = VmbUint32(0) + feats_infos = (VmbFeatureInfo * feats_count.value)() + + call_vimba_c('VmbFeatureListSelected', feats_handle, feats_name, feats_infos, feats_count, + byref(feats_found), sizeof(VmbFeatureInfo)) + + # Search selected features in given feature set + for info, feature in itertools.product(feats_infos[:feats_found.value], feats): + if info.name == feature._info.name: + result.append(feature) + + return tuple(result) + + +@TraceEnable() +def filter_features_by_name(feats: FeaturesTuple, feat_name: str): + """Search for a feature with a specific name within a feature set. + + Arguments: + feats: Feature set to search in. + feat_name: Feature name to look for. + + Returns: + The Feature with the name 'feat_name' or None if lookup failed + """ + filtered = [feat for feat in feats if feat_name == feat.get_name()] + return filtered.pop() if filtered else None + + +@TraceEnable() +def filter_features_by_type(feats: FeaturesTuple, feat_type: FeatureTypeTypes) -> FeaturesTuple: + """Search for all features with a specific type within a given feature set. + + Arguments: + feats: Feature set to search in. + feat_type: Feature Type to search for + + Returns: + A set of all features of type 'feat_type' in 'feats'. If no matching type is found an + empty set is returned. + """ + return tuple([feat for feat in feats if type(feat) == feat_type]) + + +@TraceEnable() +def filter_features_by_category(feats: FeaturesTuple, category: str) -> FeaturesTuple: + """Search for all features of a given category. + + Arguments: + feats: Feature set to search in. + category: Category to filter for + + Returns: + A set of all features of category 'category' in 'feats'. If no matching type is found an + empty set is returned. + """ + return tuple([feat for feat in feats if feat.get_category() == category]) + + +@TraceEnable() +def attach_feature_accessors(obj, feats: FeaturesTuple): + """Attach all Features in feats to obj under the feature name. + + Arguments: + obj: Object feats should be attached on. + feats: Features to attach. + """ + BLACKLIST = ( + 'PixelFormat', # PixelFormats have special access methods. + ) + + for feat in feats: + feat_name = feat.get_name() + if feat_name not in BLACKLIST: + setattr(obj, feat_name, feat) + + +@TraceEnable() +def remove_feature_accessors(obj, feats: FeaturesTuple): + """Remove all Features in feats from obj. + + Arguments: + obj: Object, feats should be removed from. + feats: Features to remove. + """ + for feat in feats: + try: + delattr(obj, feat.get_name()) + + except AttributeError: + pass + + +@TraceEnable() +def read_memory(handle: VmbHandle, addr: int, max_bytes: int) -> bytes: # coverage: skip + """Read a byte sequence from a given memory address. + + Arguments: + handle: Handle on entity that allows raw memory access. + addr: Starting address to read from. + max_bytes: Maximum number of bytes to read from addr. + + Returns: + Read memory contents as bytes. + + Raises: + ValueError if addr is negative + ValueError if max_bytes is negative. + ValueError if the memory access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + _verify_addr(addr) + _verify_size(max_bytes) + + buf = create_string_buffer(max_bytes) + bytesRead = VmbUint32() + + try: + call_vimba_c('VmbMemoryRead', handle, addr, max_bytes, buf, byref(bytesRead)) + + except VimbaCError as e: + msg = 'Memory read access at {} failed with C-Error: {}.' + raise ValueError(msg.format(hex(addr), repr(e.get_error_code()))) from e + + return buf.raw[:bytesRead.value] + + +@TraceEnable() +def write_memory(handle: VmbHandle, addr: int, data: bytes): # coverage: skip + """ Write a byte sequence to a given memory address. + + Arguments: + handle: Handle on entity that allows raw memory access. + addr: Address to write the content of 'data' too. + data: Byte sequence to write at address 'addr'. + + Raises: + ValueError if addr is negative. + ValueError if the memory access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + _verify_addr(addr) + + bytesWrite = VmbUint32() + + try: + call_vimba_c('VmbMemoryWrite', handle, addr, len(data), data, byref(bytesWrite)) + + except VimbaCError as e: + msg = 'Memory write access at {} failed with C-Error: {}.' + raise ValueError(msg.format(hex(addr), repr(e.get_error_code()))) from e + + +@TraceEnable() +def read_registers(handle: VmbHandle, addrs: Tuple[int, ...]) -> Dict[int, int]: # coverage: skip + """Read contents of multiple registers. + + Arguments: + handle: Handle on entity providing registers to access. + addrs: Sequence of addresses that should be read iteratively. + + Return: + Dictionary containing a mapping from given address to the read register values. + + Raises: + ValueError if any address in addrs is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + for addr in addrs: + _verify_addr(addr) + + size = len(addrs) + valid_reads = VmbUint32() + + c_addrs = (VmbUint64 * size)() + c_values = (VmbUint64 * size)() + + for i, addr in enumerate(addrs): + c_addrs[i] = addr + + try: + call_vimba_c('VmbRegistersRead', handle, size, c_addrs, c_values, byref(valid_reads)) + + except VimbaCError as e: + msg = 'Register read access failed with C-Error: {}.' + raise ValueError(msg.format(repr(e.get_error_code()))) from e + + return dict(zip(c_addrs, c_values)) + + +@TraceEnable() +def write_registers(handle: VmbHandle, addrs_values: Dict[int, int]): # coverage: skip + """Write data to multiple Registers. + + Arguments: + handle: Handle on entity providing registers to access. + addrs_values: Mapping between Register addresses and the data to write. + + Raises: + ValueError if any address in addrs_values is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + for addr in addrs_values: + _verify_addr(addr) + + size = len(addrs_values) + valid_writes = VmbUint32() + + addrs = (VmbUint64 * size)() + values = (VmbUint64 * size)() + + for i, addr in enumerate(addrs_values): + addrs[i] = addr + values[i] = addrs_values[addr] + + try: + call_vimba_c('VmbRegistersWrite', handle, size, addrs, values, byref(valid_writes)) + + except VimbaCError as e: + msg = 'Register write access failed with C-Error: {}.' + raise ValueError(msg.format(repr(e.get_error_code()))) from e + + +def _verify_addr(addr: int): # coverage: skip + # Note: Coverage is skipped. Function is untestable in a generic way. + if addr < 0: + raise ValueError('Given Address {} is negative'.format(addr)) + + +def _verify_size(size: int): # coverage: skip + # Note: Coverage is skipped. Function is untestable in a generic way. + if size < 0: + raise ValueError('Given size {} is negative'.format(size)) diff --git a/copylot/hardware/cameras/avt/vimba/util/__init__.py b/copylot/hardware/cameras/avt/vimba/util/__init__.py new file mode 100644 index 00000000..035a279e --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/util/__init__.py @@ -0,0 +1,72 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +# Suppress 'imported but unused' - Error from static style checker. +# flake8: noqa: F401 + +__all__ = [ + 'LogLevel', + 'LogConfig', + 'Log', + 'LOG_CONFIG_TRACE_CONSOLE_ONLY', + 'LOG_CONFIG_TRACE_FILE_ONLY', + 'LOG_CONFIG_TRACE', + 'LOG_CONFIG_INFO_CONSOLE_ONLY', + 'LOG_CONFIG_INFO_FILE_ONLY', + 'LOG_CONFIG_INFO', + 'LOG_CONFIG_WARNING_CONSOLE_ONLY', + 'LOG_CONFIG_WARNING_FILE_ONLY', + 'LOG_CONFIG_WARNING', + 'LOG_CONFIG_ERROR_CONSOLE_ONLY', + 'LOG_CONFIG_ERROR_FILE_ONLY', + 'LOG_CONFIG_ERROR', + 'LOG_CONFIG_CRITICAL_CONSOLE_ONLY', + 'LOG_CONFIG_CRITICAL_FILE_ONLY', + 'LOG_CONFIG_CRITICAL', + + # Decorators + 'TraceEnable', + 'ScopedLogEnable', + 'RuntimeTypeCheckEnable', + 'EnterContextOnCall', + 'LeaveContextOnCall', + 'RaiseIfInsideContext', + 'RaiseIfOutsideContext' +] + +from .log import Log, LogLevel, LogConfig, LOG_CONFIG_TRACE_CONSOLE_ONLY, \ + LOG_CONFIG_TRACE_FILE_ONLY, LOG_CONFIG_TRACE, LOG_CONFIG_INFO_CONSOLE_ONLY, \ + LOG_CONFIG_INFO_FILE_ONLY, LOG_CONFIG_INFO, LOG_CONFIG_WARNING_CONSOLE_ONLY, \ + LOG_CONFIG_WARNING_FILE_ONLY, LOG_CONFIG_WARNING, LOG_CONFIG_ERROR_CONSOLE_ONLY, \ + LOG_CONFIG_ERROR_FILE_ONLY, LOG_CONFIG_ERROR, LOG_CONFIG_CRITICAL_CONSOLE_ONLY, \ + LOG_CONFIG_CRITICAL_FILE_ONLY, LOG_CONFIG_CRITICAL + +from .tracer import TraceEnable +from .scoped_log import ScopedLogEnable +from .runtime_type_check import RuntimeTypeCheckEnable +from .context_decorator import EnterContextOnCall, LeaveContextOnCall, RaiseIfInsideContext, \ + RaiseIfOutsideContext diff --git a/copylot/hardware/cameras/avt/vimba/util/context_decorator.py b/copylot/hardware/cameras/avt/vimba/util/context_decorator.py new file mode 100755 index 00000000..698553d9 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/util/context_decorator.py @@ -0,0 +1,96 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import functools + +__all__ = [ + 'EnterContextOnCall', + 'LeaveContextOnCall', + 'RaiseIfInsideContext', + 'RaiseIfOutsideContext' +] + + +class EnterContextOnCall: + """Decorator setting/injecting flag used for checking the context.""" + def __call__(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + args[0]._context_entered = True + return func(*args, **kwargs) + + return wrapper + + +class LeaveContextOnCall: + """Decorator clearing/injecting flag used for checking the context.""" + def __call__(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + args[0]._context_entered = False + return result + + return wrapper + + +class RaiseIfInsideContext: + """Raising RuntimeError is decorated Method is called inside with-statement. + + Note This Decorator shall work only on Object implementing a Context Manger. + For this to work object must offer a boolean attribute called _context_entered + """ + def __call__(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if args[0]._context_entered: + msg = 'Called \'{}()\' inside of \'with\' - statement scope.' + msg = msg.format('{}'.format(func.__qualname__)) + raise RuntimeError(msg) + + return func(*args, **kwargs) + + return wrapper + + +class RaiseIfOutsideContext: + """Raising RuntimeError is decorated Method is called outside with-statement. + + Note This Decorator shall work only on Object implementing a Context Manger. + For this to work object must offer a boolean attribute called __context_entered + """ + def __call__(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not args[0]._context_entered: + msg = 'Called \'{}()\' outside of \'with\' - statement scope.' + msg = msg.format('{}'.format(func.__qualname__)) + raise RuntimeError(msg) + + return func(*args, **kwargs) + + return wrapper diff --git a/copylot/hardware/cameras/avt/vimba/util/log.py b/copylot/hardware/cameras/avt/vimba/util/log.py new file mode 100644 index 00000000..21d3500a --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/util/log.py @@ -0,0 +1,295 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import os +import enum +import datetime +import logging + +from typing import List, Optional + + +__all__ = [ + 'LogLevel', + 'LogConfig', + 'Log', + 'LOG_CONFIG_TRACE_CONSOLE_ONLY', + 'LOG_CONFIG_TRACE_FILE_ONLY', + 'LOG_CONFIG_TRACE', + 'LOG_CONFIG_INFO_CONSOLE_ONLY', + 'LOG_CONFIG_INFO_FILE_ONLY', + 'LOG_CONFIG_INFO', + 'LOG_CONFIG_WARNING_CONSOLE_ONLY', + 'LOG_CONFIG_WARNING_FILE_ONLY', + 'LOG_CONFIG_WARNING', + 'LOG_CONFIG_ERROR_CONSOLE_ONLY', + 'LOG_CONFIG_ERROR_FILE_ONLY', + 'LOG_CONFIG_ERROR', + 'LOG_CONFIG_CRITICAL_CONSOLE_ONLY', + 'LOG_CONFIG_CRITICAL_FILE_ONLY', + 'LOG_CONFIG_CRITICAL' +] + + +class LogLevel(enum.IntEnum): + """Enum containing all LogLevels. + + Enum values are: + Trace - Show Tracing information. Show all messages. + Info - Show Informational, Warning, Error, and Critical Events. + Warning - Show Warning, Error, and Critical Events. + Error - Show Errors and Critical Events. + Critical - Show Critical Events only. + """ + Trace = logging.DEBUG + Info = logging.INFO + Warning = logging.WARNING + Error = logging.ERROR + Critical = logging.CRITICAL + + def __str__(self): + return self._name_ + + def as_equal_len_str(self) -> str: + return _LEVEL_TO_EQUAL_LEN_STR[self] + + +_LEVEL_TO_EQUAL_LEN_STR = { + LogLevel.Trace: 'Trace ', + LogLevel.Info: 'Info ', + LogLevel.Warning: 'Warning ', + LogLevel.Error: 'Error ', + LogLevel.Critical: 'Critical' +} + + +class LogConfig: + """The LogConfig is a builder to configure various specialized logging configurations. + The constructed LogConfig must set via vimba.Vimba or the ScopedLogEnable Decorator + to start logging. + """ + + __ENTRY_FORMAT = logging.Formatter('%(asctime)s | %(message)s') + + def __init__(self): + self.__handlers: List[logging.Handler] = [] + self.__max_msg_length: Optional[int] = None + + def add_file_log(self, level: LogLevel) -> 'LogConfig': + """Add a new Log file to the Config Builder. + + Arguments: + level: LogLevel of the added log file. + + Returns: + Reference to the LogConfig instance (builder pattern). + """ + log_ts = datetime.datetime.today().strftime('%Y-%m-%d_%H-%M-%S') + log_file = 'VimbaPython_{}_{}.log'.format(log_ts, str(level)) + log_file = os.path.join(os.getcwd(), log_file) + + handler = logging.FileHandler(log_file, delay=True) + handler.setLevel(level) + handler.setFormatter(LogConfig.__ENTRY_FORMAT) + + self.__handlers.append(handler) + return self + + def add_console_log(self, level: LogLevel) -> 'LogConfig': + """Add a new Console Log to the Config Builder. + + Arguments: + level: LogLevel of the added console log file. + + Returns: + Reference to the LogConfig instance (builder pattern). + """ + handler = logging.StreamHandler() + handler.setLevel(level) + handler.setFormatter(LogConfig.__ENTRY_FORMAT) + + self.__handlers.append(handler) + return self + + def set_max_msg_length(self, max_msg_length: int): + """Set max length of a log entry. Messages longer than this entry will be cut off.""" + self.__max_msg_length = max_msg_length + + def get_max_msg_length(self) -> Optional[int]: + """Get configured max message length""" + return self.__max_msg_length + + def get_handlers(self) -> List[logging.Handler]: + """Get all configured log handlers""" + return self.__handlers + + +class Log: + class __Impl: + """This class is wraps the logging Facility. Since this is as Singleton + Use Log.get_instace(), to access the log. + """ + def __init__(self): + """Do not call directly. Use Log.get_instance() instead.""" + self.__logger: Optional[logging.Logger] = None + self.__config: Optional[LogConfig] = None + self._test_buffer: Optional[List[str]] = None + + def __bool__(self): + return bool(self.__logger) + + def enable(self, config: LogConfig): + """Enable global VimbaPython logging mechanism. + + Arguments: + config: The configuration to apply. + """ + self.disable() + + logger = logging.getLogger('VimbaPythonLog') + logger.setLevel(logging.DEBUG) + + for handler in config.get_handlers(): + logger.addHandler(handler) + + self.__config = config + self.__logger = logger + + def disable(self): + """Disable global VimbaPython logging mechanism.""" + if self.__logger and self.__config: + for handler in self.__config.get_handlers(): + handler.close() + self.__logger.removeHandler(handler) + + self.__logger = None + self.__config = None + + def get_config(self) -> Optional[LogConfig]: + """ Get log configuration + + Returns: + Configuration if the log is enabled. In case the log is disabled return None. + """ + return self.__config + + def trace(self, msg: str): + """Add an entry of LogLevel.Trace to the log. Does nothing is the log is disabled. + + Arguments: + msg - The message that should be added to the Log. + """ + if self.__logger: + self.__logger.debug(self.__build_msg(LogLevel.Trace, msg)) + + def info(self, msg: str): + """Add an entry of LogLevel.Info to the log. Does nothing is the log is disabled. + + Arguments: + msg - The message that should be added to the Log. + """ + if self.__logger: + self.__logger.info(self.__build_msg(LogLevel.Info, msg)) + + def warning(self, msg: str): + """Add an entry of LogLevel.Warning to the log. Does nothing is the log is disabled. + + Arguments: + msg - The message that should be added to the Log. + """ + if self.__logger: + self.__logger.warning(self.__build_msg(LogLevel.Warning, msg)) + + def error(self, msg: str): + """Add an entry of LogLevel.Error to the log. Does nothing is the log is disabled. + + Arguments: + msg - The message that should be added to the Log. + """ + if self.__logger: + self.__logger.error(self.__build_msg(LogLevel.Error, msg)) + + def critical(self, msg: str): + """Add an entry of LogLevel.Critical to the log. Does nothing is the log is disabled. + + Arguments: + msg - The message that should be added to the Log. + """ + if self.__logger: + self.__logger.critical(self.__build_msg(LogLevel.Critical, msg)) + + def __build_msg(self, loglevel: LogLevel, msg: str) -> str: + msg = '{} | {}'.format(loglevel.as_equal_len_str(), msg) + max_len = self.__config.get_max_msg_length() if self.__config else None + + if max_len and (max_len < len(msg)): + suffix = ' ...' + msg = msg[:max_len - len(suffix)] + suffix + + if self._test_buffer is not None: + self._test_buffer.append(msg) + + return msg + + __instance = __Impl() + + @staticmethod + def get_instance() -> '__Impl': + """Get Log instance.""" + return Log.__instance + + +def _build_cfg(console_level: Optional[LogLevel], file_level: Optional[LogLevel]) -> LogConfig: + cfg = LogConfig() + + cfg.set_max_msg_length(200) + + if console_level: + cfg.add_console_log(console_level) + + if file_level: + cfg.add_file_log(file_level) + + return cfg + + +# Exported Default Log configurations. +LOG_CONFIG_TRACE_CONSOLE_ONLY = _build_cfg(LogLevel.Trace, None) +LOG_CONFIG_TRACE_FILE_ONLY = _build_cfg(None, LogLevel.Trace) +LOG_CONFIG_TRACE = _build_cfg(LogLevel.Trace, LogLevel.Trace) +LOG_CONFIG_INFO_CONSOLE_ONLY = _build_cfg(LogLevel.Info, None) +LOG_CONFIG_INFO_FILE_ONLY = _build_cfg(None, LogLevel.Info) +LOG_CONFIG_INFO = _build_cfg(LogLevel.Info, LogLevel.Info) +LOG_CONFIG_WARNING_CONSOLE_ONLY = _build_cfg(LogLevel.Warning, None) +LOG_CONFIG_WARNING_FILE_ONLY = _build_cfg(None, LogLevel.Warning) +LOG_CONFIG_WARNING = _build_cfg(LogLevel.Warning, LogLevel.Warning) +LOG_CONFIG_ERROR_CONSOLE_ONLY = _build_cfg(LogLevel.Error, None) +LOG_CONFIG_ERROR_FILE_ONLY = _build_cfg(None, LogLevel.Error) +LOG_CONFIG_ERROR = _build_cfg(LogLevel.Error, LogLevel.Error) +LOG_CONFIG_CRITICAL_CONSOLE_ONLY = _build_cfg(LogLevel.Critical, None) +LOG_CONFIG_CRITICAL_FILE_ONLY = _build_cfg(None, LogLevel.Critical) +LOG_CONFIG_CRITICAL = _build_cfg(LogLevel.Critical, LogLevel.Critical) diff --git a/copylot/hardware/cameras/avt/vimba/util/runtime_type_check.py b/copylot/hardware/cameras/avt/vimba/util/runtime_type_check.py new file mode 100644 index 00000000..09553e28 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/util/runtime_type_check.py @@ -0,0 +1,223 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import collections.abc + +from inspect import isfunction, ismethod, signature +from functools import wraps +from typing import get_type_hints, Union +from .log import Log + + +__all__ = [ + 'RuntimeTypeCheckEnable' +] + + +class RuntimeTypeCheckEnable: + """Decorator adding runtime type checking to the wrapped callable. + + Each time the callable is executed, all arguments are checked if they match with the given + type hints. If all checks are passed, the wrapped function is executed, if the given + arguments to not match a TypeError is raised. + Note: This decorator is no replacement for a feature complete TypeChecker. It supports only + a subset of all types expressible by type hints. + """ + _log = Log.get_instance() + + def __call__(self, func): + @wraps(func) + def wrapper(*args, **kwargs): + full_args, hints = self.__dismantle_sig(func, *args, **kwargs) + + for arg_name in hints: + self.__verify_arg(func, hints[arg_name], (arg_name, full_args[arg_name])) + + return func(*args, **kwargs) + + return wrapper + + def __dismantle_sig(self, func, *args, **kwargs): + # Get merge args, kwargs and defaults to complete argument list. + full_args = signature(func).bind(*args, **kwargs) + full_args.apply_defaults() + + # Get available type hints, remove return value. + hints = get_type_hints(func) + hints.pop('return', None) + + return (full_args.arguments, hints) + + def __verify_arg(self, func, type_hint, arg_spec): + arg_name, arg = arg_spec + + if (self.__matches(type_hint, arg)): + return + + msg = '\'{}\' called with unexpected argument type. Argument\'{}\'. Expected type: {}.' + msg = msg.format(func.__qualname__, arg_name, type_hint) + + RuntimeTypeCheckEnable._log.error(msg) + raise TypeError(msg) + + def __matches(self, type_hint, arg) -> bool: + if self.__matches_base_types(type_hint, arg): + return True + + elif self.__matches_type_types(type_hint, arg): + return True + + elif self.__matches_union_types(type_hint, arg): + return True + + elif self.__matches_tuple_types(type_hint, arg): + return True + + elif self.__matches_dict_types(type_hint, arg): + return True + + else: + return self.__matches_callable(type_hint, arg) + + def __matches_base_types(self, type_hint, arg) -> bool: + return type_hint == type(arg) + + def __matches_type_types(self, type_hint, arg) -> bool: + try: + if not type_hint.__origin__ == type: + return False + + hint_args = type_hint.__args__ + + except AttributeError: + return False + + return arg in hint_args + + def __matches_union_types(self, type_hint, arg) -> bool: + try: + if not type_hint.__origin__ == Union: + return False + + except AttributeError: + return False + + # If Matches if true for an Union hint: + for hint in type_hint.__args__: + if self.__matches(hint, arg): + return True + + return False + + def __matches_tuple_types(self, type_hint, arg) -> bool: + try: + if not (type_hint.__origin__ == tuple and type(arg) == tuple): + return False + + except AttributeError: + return False + + if arg == (): + return True + + if Ellipsis in type_hint.__args__: + fn = self.__matches_var_length_tuple + + else: + fn = self.__matches_fixed_size_tuple + + return fn(type_hint, arg) + + def __matches_fixed_size_tuple(self, type_hint, arg) -> bool: + # To pass, the entire tuple must match in length and all types + expand_hint = type_hint.__args__ + + if len(expand_hint) != len(arg): + return False + + for hint, value in zip(expand_hint, arg): + if not self.__matches(hint, value): + return False + + return True + + def __matches_var_length_tuple(self, type_hint, arg) -> bool: + # To pass a tuple can be empty or all contents must match the given type. + hint, _ = type_hint.__args__ + + for value in arg: + if not self.__matches(hint, value): + return False + + return True + + def __matches_dict_types(self, type_hint, arg) -> bool: + # To pass the hint must be a Dictionary and arg must match the given types. + try: + if not (type_hint.__origin__ == dict and type(arg) == dict): + return False + + except AttributeError: + return False + + key_type, val_type = type_hint.__args__ + + for k, v in arg.items(): + if type(k) != key_type or type(v) != val_type: + return False + + return True + + def __matches_callable(self, type_hint, arg) -> bool: + # Return if the given hint is no callable + try: + if not type_hint.__origin__ == collections.abc.Callable: + return False + + except AttributeError: + return False + + # Verify that are is some form of callable.: + # 1) Check if it is either a function or a method + # 2) If it is an object, check if it has a __call__ method. If so use call for checks. + if not (isfunction(arg) or ismethod(arg)): + + try: + arg = getattr(arg, '__call__') + + except AttributeError: + return False + + # Examine signature of given callable + sig_args = signature(arg).parameters + hint_args = type_hint.__args__ + + # Verify Parameter list length + if len(sig_args) != len(hint_args[:-1]): + return False + + return True diff --git a/copylot/hardware/cameras/avt/vimba/util/scoped_log.py b/copylot/hardware/cameras/avt/vimba/util/scoped_log.py new file mode 100644 index 00000000..93d79208 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/util/scoped_log.py @@ -0,0 +1,80 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from functools import wraps +from typing import Any, Callable, Tuple, Optional +from .log import LogConfig, Log + + +__all__ = [ + 'ScopedLogEnable' +] + + +class _ScopedLog: + __log = Log.get_instance() + + def __init__(self, config: LogConfig): + self.__config: LogConfig = config + self.__old_config: Optional[LogConfig] = None + + def __enter__(self): + self.__old_config = _ScopedLog.__log.get_config() + _ScopedLog.__log.enable(self.__config) + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + if self.__old_config: + _ScopedLog.__log.enable(self.__old_config) + + else: + _ScopedLog.__log.disable() + + +class ScopedLogEnable: + """Decorator: Enables logging facility before execution of the wrapped function + and disables logging after exiting the wrapped function. This allows more specific + logging of a code section compared to enabling or disabling the global logging mechanism. + + Arguments: + config: The configuration the log should be enabled with. + """ + def __init__(self, config: LogConfig): + """Add scoped logging to a Callable. + + Arguments: + config: The configuration the log should be enabled with. + """ + self.__config = config + + def __call__(self, func: Callable[..., Any]): + @wraps(func) + def wrapper(*args: Tuple[Any, ...]): + with _ScopedLog(self.__config): + return func(*args) + + return wrapper diff --git a/copylot/hardware/cameras/avt/vimba/util/tracer.py b/copylot/hardware/cameras/avt/vimba/util/tracer.py new file mode 100644 index 00000000..e1b2ae32 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/util/tracer.py @@ -0,0 +1,136 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from functools import reduce, wraps +from inspect import signature +from .log import Log + + +__all__ = [ + 'TraceEnable' +] + + +_FMT_MSG_ENTRY: str = 'Enter | {}' +_FMT_MSG_LEAVE: str = 'Leave | {}' +_FMT_MSG_RAISE: str = 'Raise | {}, {}' +_FMT_ERROR: str = 'ErrorType: {}, ErrorValue: {}' +_INDENT_PER_LEVEL: str = ' ' + + +def _args_to_str(func, *args, **kwargs) -> str: + # Expand function signature + sig = signature(func).bind(*args, **kwargs) + sig.apply_defaults() + full_args = sig.arguments + + # Early return if there is nothing to print + if not full_args: + return '(None)' + + def fold(args_as_str: str, arg): + name, value = arg + + if name == 'self': + arg_str = 'self' + + else: + arg_str = str(value) + + return '{}{}, '.format(args_as_str, arg_str) + + return '({})'.format(reduce(fold, full_args.items(), '')[:-2]) + + +def _get_indent(level: int) -> str: + return _INDENT_PER_LEVEL * level + + +def _create_enter_msg(name: str, level: int, args_str: str) -> str: + msg = '{}{}{}'.format(_get_indent(level), name, args_str) + return _FMT_MSG_ENTRY.format(msg) + + +def _create_leave_msg(name: str, level: int, ) -> str: + msg = '{}{}'.format(_get_indent(level), name) + return _FMT_MSG_LEAVE.format(msg) + + +def _create_raise_msg(name: str, level: int, exc_type: Exception, exc_value: str) -> str: + msg = '{}{}'.format(_get_indent(level), name) + exc = _FMT_ERROR.format(exc_type, exc_value) + return _FMT_MSG_RAISE.format(msg, exc) + + +class _Tracer: + __log = Log.get_instance() + __level: int = 0 + + @staticmethod + def is_log_enabled() -> bool: + return bool(_Tracer.__log) + + def __init__(self, func, *args, **kwargs): + self.__full_name: str = '{}.{}'.format(func.__module__, func.__qualname__) + self.__full_args: str = _args_to_str(func, *args, **kwargs) + + def __enter__(self): + msg = _create_enter_msg(self.__full_name, _Tracer.__level, self.__full_args) + + _Tracer.__log.trace(msg) + _Tracer.__level += 1 + + def __exit__(self, exc_type, exc_value, exc_traceback): + _Tracer.__level -= 1 + + if exc_type: + msg = _create_raise_msg(self.__full_name, _Tracer.__level, exc_type, exc_value) + + else: + msg = _create_leave_msg(self.__full_name, _Tracer.__level) + + _Tracer.__log.trace(msg) + + +class TraceEnable: + """Decorator: Adds an entry of LogLevel. Trace on entry and exit of the wrapped function. + On exit, the log entry contains information if the function was left normally or with an + exception. + """ + def __call__(self, func): + @wraps(func) + def wrapper(*args, **kwargs): + if _Tracer.is_log_enabled(): + with _Tracer(func, *args, **kwargs): + result = func(*args, **kwargs) + + return result + + else: + return func(*args, **kwargs) + + return wrapper diff --git a/copylot/hardware/cameras/avt/vimba/vimba.py b/copylot/hardware/cameras/avt/vimba/vimba.py new file mode 100644 index 00000000..20c77131 --- /dev/null +++ b/copylot/hardware/cameras/avt/vimba/vimba.py @@ -0,0 +1,600 @@ +"""BSD 2-Clause License + +Copyright (c) 2019, Allied Vision Technologies GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import threading +from typing import List, Dict, Tuple +from .c_binding import call_vimba_c, VIMBA_C_VERSION, VIMBA_IMAGE_TRANSFORM_VERSION, \ + G_VIMBA_C_HANDLE +from .feature import discover_features, FeatureTypes, FeaturesTuple, FeatureTypeTypes, EnumFeature +from .shared import filter_features_by_name, filter_features_by_type, filter_affected_features, \ + filter_selected_features, filter_features_by_category, \ + attach_feature_accessors, remove_feature_accessors, read_memory, \ + write_memory, read_registers, write_registers +from .interface import Interface, InterfaceChangeHandler, InterfaceEvent, InterfacesTuple, \ + InterfacesList, discover_interfaces, discover_interface +from .camera import Camera, CamerasList, CameraChangeHandler, CameraEvent, CamerasTuple, \ + discover_cameras, discover_camera +from .util import Log, LogConfig, TraceEnable, RuntimeTypeCheckEnable, EnterContextOnCall, \ + LeaveContextOnCall, RaiseIfInsideContext, RaiseIfOutsideContext +from .error import VimbaCameraError, VimbaInterfaceError, VimbaFeatureError +from . import __version__ as VIMBA_PYTHON_VERSION + + +__all__ = [ + 'Vimba', +] + + +class Vimba: + class __Impl: + """This class allows access to the entire Vimba System. + Vimba is meant be used in conjunction with the "with" - Statement, upon + entering the context, all system features, connected cameras and interfaces are detected + and can be used. + """ + + @TraceEnable() + @LeaveContextOnCall() + def __init__(self): + """Do not call directly. Use Vimba.get_instance() instead.""" + self.__feats: FeaturesTuple = () + + self.__inters: InterfacesList = () + self.__inters_lock: threading.Lock = threading.Lock() + self.__inters_handlers: List[InterfaceChangeHandler] = [] + self.__inters_handlers_lock: threading.Lock = threading.Lock() + + self.__cams: CamerasList = () + self.__cams_lock: threading.Lock = threading.Lock() + self.__cams_handlers: List[CameraChangeHandler] = [] + self.__cams_handlers_lock: threading.Lock = threading.Lock() + + self.__nw_discover: bool = True + self.__context_cnt: int = 0 + + @TraceEnable() + def __enter__(self): + if not self.__context_cnt: + self._startup() + + self.__context_cnt += 1 + return self + + @TraceEnable() + def __exit__(self, exc_type, exc_value, exc_traceback): + self.__context_cnt -= 1 + + if not self.__context_cnt: + self._shutdown() + + def get_version(self) -> str: + """ Returns version string of VimbaPython and underlaying dependencies.""" + msg = 'VimbaPython: {} (using VimbaC: {}, VimbaImageTransform: {})' + return msg.format(VIMBA_PYTHON_VERSION, VIMBA_C_VERSION, VIMBA_IMAGE_TRANSFORM_VERSION) + + @RaiseIfInsideContext() + @RuntimeTypeCheckEnable() + def set_network_discovery(self, enable: bool): + """Enable/Disable network camera discovery. + + Arguments: + enable - If 'True' VimbaPython tries to detect cameras connected via Ethernet + on entering the 'with' statement. If set to 'False', no network + discover occurs. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError if called inside with-statement. + """ + self.__nw_discover = enable + + @RuntimeTypeCheckEnable() + def enable_log(self, config: LogConfig): + """Enable VimbaPython's logging mechanism. + + Arguments: + config - Configuration for the logging mechanism. + + Raises: + TypeError if parameters do not match their type hint. + """ + Log.get_instance().enable(config) + + def disable_log(self): + """Disable VimbaPython's logging mechanism.""" + Log.get_instance().disable() + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def read_memory(self, addr: int, max_bytes: int) -> bytes: # coverage: skip + """Read a byte sequence from a given memory address. + + Arguments: + addr: Starting address to read from. + max_bytes: Maximum number of bytes to read from addr. + + Returns: + Read memory contents as bytes. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + ValueError if addr is negative + ValueError if max_bytes is negative. + ValueError if the memory access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return read_memory(G_VIMBA_C_HANDLE, addr, max_bytes) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def write_memory(self, addr: int, data: bytes): # coverage: skip + """ Write a byte sequence to a given memory address. + + Arguments: + addr: Address to write the content of 'data' too. + data: Byte sequence to write at address 'addr'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + ValueError if addr is negative. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return write_memory(G_VIMBA_C_HANDLE, addr, data) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def read_registers(self, addrs: Tuple[int, ...]) -> Dict[int, int]: # coverage: skip + """Read contents of multiple registers. + + Arguments: + addrs: Sequence of addresses that should be read iteratively. + + Return: + Dictionary containing a mapping from given address to the read register values. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + ValueError if any address in addrs_values is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return read_registers(G_VIMBA_C_HANDLE, addrs) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def write_registers(self, addrs_values: Dict[int, int]): # coverage: skip + """Write data to multiple Registers. + + Arguments: + addrs_values: Mapping between Register addresses and the data to write. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + ValueError if any address in addrs is negative. + ValueError if the register access was invalid. + """ + # Note: Coverage is skipped. Function is untestable in a generic way. + return write_registers(G_VIMBA_C_HANDLE, addrs_values) + + @RaiseIfOutsideContext() + def get_all_interfaces(self) -> InterfacesTuple: + """Get access to all discovered Interfaces: + + Returns: + A set of all currently detected Interfaces. + + Raises: + RuntimeError then called outside of "with" - statement. + """ + with self.__inters_lock: + return tuple(self.__inters) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_interface_by_id(self, id_: str) -> Interface: + """Lookup Interface with given ID. + + Arguments: + id_ - Interface Id to search for. + + Returns: + Interface associated with given Id. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + VimbaInterfaceError if interface with id_ can't be found. + """ + with self.__inters_lock: + inter = [inter for inter in self.__inters if id_ == inter.get_id()] + + if not inter: + raise VimbaInterfaceError('Interface with ID \'{}\' not found.'.format(id_)) + + return inter.pop() + + @RaiseIfOutsideContext() + def get_all_cameras(self) -> CamerasTuple: + """Get access to all discovered Cameras. + + Returns: + A set of all currently detected Cameras. + + Raises: + RuntimeError then called outside of "with" - statement. + """ + with self.__cams_lock: + return tuple(self.__cams) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_camera_by_id(self, id_: str) -> Camera: + """Lookup Camera with given ID. + + Arguments: + id_ - Camera Id to search for. For GigE - Cameras, the IP and MAC-Address + can be used to Camera lookup + + Returns: + Camera associated with given Id. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + VimbaCameraError if camera with id_ can't be found. + """ + with self.__cams_lock: + # Search for given Camera Id in all currently detected cameras. + for cam in self.__cams: + if id_ == cam.get_id(): + return cam + + # If a search by ID fails, the given id_ is almost certain an IP or MAC - Address. + # Try to query this Camera. + try: + cam_info = discover_camera(id_) + + # Since cam_info is newly constructed, search in existing cameras for a Camera + for cam in self.__cams: + if cam_info.get_id() == cam.get_id(): + return cam + + except VimbaCameraError: + pass + + raise VimbaCameraError('No Camera with Id \'{}\' available.'.format(id_)) + + @RaiseIfOutsideContext() + def get_all_features(self) -> FeaturesTuple: + """Get access to all discovered system features: + + Returns: + A set of all currently detected Features. + + Raises: + RuntimeError then called outside of "with" - statement. + """ + return self.__feats + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_affected_by(self, feat: FeatureTypes) -> FeaturesTuple: + """Get all system features affected by a specific system feature. + + Arguments: + feat - Feature used find features that are affected by feat. + + Returns: + A set of features affected by changes on 'feat'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + VimbaFeatureError if 'feat' is not a system feature. + """ + return filter_affected_features(self.__feats, feat) + + @TraceEnable() + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_selected_by(self, feat: FeatureTypes) -> FeaturesTuple: + """Get all system features selected by a specific system feature. + + Arguments: + feat - Feature used find features that are selected by feat. + + Returns: + A set of features selected by 'feat'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + VimbaFeatureError if 'feat' is not a system feature. + """ + return filter_selected_features(self.__feats, feat) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_type(self, feat_type: FeatureTypeTypes) -> FeaturesTuple: + """Get all system features of a specific feature type. + + Valid FeatureTypes are: IntFeature, FloatFeature, StringFeature, BoolFeature, + EnumFeature, CommandFeature, RawFeature + + Arguments: + feat_type - FeatureType used find features of that type. + + Returns: + A set of features of type 'feat_type'. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + """ + return filter_features_by_type(self.__feats, feat_type) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_features_by_category(self, category: str) -> FeaturesTuple: + """Get all system features of a specific category. + + Arguments: + category - Category that should be used for filtering. + + Returns: + A set of features of category 'category'. + + Returns: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + """ + return filter_features_by_category(self.__feats, category) + + @RaiseIfOutsideContext() + @RuntimeTypeCheckEnable() + def get_feature_by_name(self, feat_name: str) -> FeatureTypes: + """Get a system feature by its name. + + Arguments: + feat_name - Name used to find a feature. + + Returns: + Feature with the associated name. + + Raises: + TypeError if parameters do not match their type hint. + RuntimeError then called outside of "with" - statement. + VimbaFeatureError if no feature is associated with 'feat_name'. + """ + feat = filter_features_by_name(self.__feats, feat_name) + + if not feat: + raise VimbaFeatureError('Feature \'{}\' not found.'.format(feat_name)) + + return feat + + @RuntimeTypeCheckEnable() + def register_camera_change_handler(self, handler: CameraChangeHandler): + """Add Callable what is executed on camera connect/disconnect + + Arguments: + handler - The change handler that shall be added. + + Raises: + TypeError if parameters do not match their type hint. + """ + with self.__cams_handlers_lock: + if handler not in self.__cams_handlers: + self.__cams_handlers.append(handler) + + def unregister_all_camera_change_handlers(self): + """Remove all currently registered camera change handlers""" + with self.__cams_handlers_lock: + if self.__cams_handlers: + self.__cams_handlers.clear() + + @RuntimeTypeCheckEnable() + def unregister_camera_change_handler(self, handler: CameraChangeHandler): + """Remove previously registered camera change handler + + Arguments: + handler - The change handler that shall be removed. + + Raises: + TypeError if parameters do not match their type hint. + """ + with self.__cams_handlers_lock: + if handler in self.__cams_handlers: + self.__cams_handlers.remove(handler) + + @RuntimeTypeCheckEnable() + def register_interface_change_handler(self, handler: InterfaceChangeHandler): + """Add Callable what is executed on interface connect/disconnect + + Arguments: + handler - The change handler that shall be added. + + Raises: + TypeError if parameters do not match their type hint. + """ + with self.__inters_handlers_lock: + if handler not in self.__inters_handlers: + self.__inters_handlers.append(handler) + + def unregister_all_interface_change_handlers(self): + """Remove all currently registered interface change handlers""" + with self.__inters_handlers_lock: + if self.__inters_handlers: + self.__inters_handlers.clear() + + @RuntimeTypeCheckEnable() + def unregister_interface_change_handler(self, handler: InterfaceChangeHandler): + """Remove previously registered interface change handler + + Arguments: + handler - The change handler that shall be removed. + + Raises: + TypeError if parameters do not match their type hint. + """ + with self.__inters_handlers_lock: + if handler in self.__inters_handlers: + self.__inters_handlers.remove(handler) + + @TraceEnable() + @EnterContextOnCall() + def _startup(self): + Log.get_instance().info('Starting {}'.format(self.get_version())) + + call_vimba_c('VmbStartup') + + self.__inters = discover_interfaces() + self.__cams = discover_cameras(self.__nw_discover) + self.__feats = discover_features(G_VIMBA_C_HANDLE) + attach_feature_accessors(self, self.__feats) + + feat = self.get_feature_by_name('DiscoveryInterfaceEvent') + feat.register_change_handler(self.__inter_cb_wrapper) + + feat = self.get_feature_by_name('DiscoveryCameraEvent') + feat.register_change_handler(self.__cam_cb_wrapper) + + @TraceEnable() + @LeaveContextOnCall() + def _shutdown(self): + self.unregister_all_camera_change_handlers() + self.unregister_all_interface_change_handlers() + + for feat in self.__feats: + feat.unregister_all_change_handlers() + + remove_feature_accessors(self, self.__feats) + self.__feats = () + self.__cams_handlers = [] + self.__cams = () + self.__inters_handlers = [] + self.__inters = () + + call_vimba_c('VmbShutdown') + + def __cam_cb_wrapper(self, cam_event: EnumFeature): # coverage: skip + # Skip coverage because it can't be measured. This is called from C-Context + event = CameraEvent(int(cam_event.get())) + cam = None + cam_id = self.get_feature_by_name('DiscoveryCameraIdent').get() + log = Log.get_instance() + + # New camera found: Add it to camera list + if event == CameraEvent.Detected: + cam = discover_camera(cam_id) + + with self.__cams_lock: + self.__cams.append(cam) + + log.info('Added camera \"{}\" to active cameras'.format(cam_id)) + + # Existing camera lost. Remove it from active cameras + elif event == CameraEvent.Missing: + with self.__cams_lock: + cam = [c for c in self.__cams if cam_id == c.get_id()].pop() + cam._disconnected = True + self.__cams.remove(cam) + + log.info('Removed camera \"{}\" from active cameras'.format(cam_id)) + + else: + cam = self.get_camera_by_id(cam_id) + + with self.__cams_handlers_lock: + for handler in self.__cams_handlers: + try: + handler(cam, event) + + except Exception as e: + msg = 'Caught Exception in handler: ' + msg += 'Type: {}, '.format(type(e)) + msg += 'Value: {}, '.format(e) + msg += 'raised by: {}'.format(handler) + Log.get_instance().error(msg) + raise e + + def __inter_cb_wrapper(self, inter_event: EnumFeature): # coverage: skip + # Skip coverage because it can't be measured. This is called from C-Context + event = InterfaceEvent(int(inter_event.get())) + inter = None + inter_id = self.get_feature_by_name('DiscoveryInterfaceIdent').get() + log = Log.get_instance() + + # New interface found: Add it to interface list + if event == InterfaceEvent.Detected: + inter = discover_interface(inter_id) + + with self.__inters_lock: + self.__inters.append(inter) + + log.info('Added interface \"{}\" to active interfaces'.format(inter_id)) + + # Existing interface lost. Remove it from active interfaces + elif event == InterfaceEvent.Missing: + with self.__inters_lock: + inter = [i for i in self.__inters if inter_id == i.get_id()].pop() + self.__inters.remove(inter) + + log.info('Removed interface \"{}\" from active interfaces'.format(inter_id)) + + else: + inter = self.get_interface_by_id(inter_id) + + with self.__inters_handlers_lock: + for handler in self.__inters_handlers: + try: + handler(inter, event) + + except Exception as e: + msg = 'Caught Exception in handler: ' + msg += 'Type: {}, '.format(type(e)) + msg += 'Value: {}, '.format(e) + msg += 'raised by: {}'.format(handler) + Log.get_instance().error(msg) + raise e + + __instance = __Impl() + + @staticmethod + @TraceEnable() + def get_instance() -> '__Impl': + """Get VimbaSystem Singleton.""" + return Vimba.__instance diff --git a/copylot/hardware/cameras/orca/camera.py b/copylot/hardware/cameras/orca/camera.py index 5c387e6e..2b87163b 100644 --- a/copylot/hardware/cameras/orca/camera.py +++ b/copylot/hardware/cameras/orca/camera.py @@ -17,7 +17,6 @@ class OrcaCamera(AbstractCamera): """ def __init__(self, camera_index: int = 0): - self._camera_index = camera_index def run(self, nb_frame: int = 100000): @@ -36,7 +35,6 @@ def run(self, nb_frame: int = 100000): if dcam.dev_open(): nb_buffer_frames = 3 if dcam.buf_alloc(nb_buffer_frames): - if dcam.cap_start(): timeout_milisec = 20 diff --git a/docs/Makefile b/docs/Makefile index 12856770..95714bf6 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,7 +20,7 @@ help: clean: -rm -rf $(BUILDDIR)/* -html: +build: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." \ No newline at end of file + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 00000000..29a11e06 --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1,5 @@ +numpydoc==1.1.0 +sphinx==4.2.0 +sphinx-rtd-theme==1.0.0 +sphinx-copybutton==0.4.0 +sphinx-multiversion==0.2.4 \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index daded665..8b55be50 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,6 +50,16 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] +html_sidebars = { + '**': [ + 'versions.html', + ], +} + +# for now, only target the main branch (and ignore tags) +smv_branch_whitelist = 'main' +# smv_tag_whitelist = r'^v\d+\.\d+\.\d+$' + # The suffix of source filenames. source_suffix = ".rst" diff --git a/requirements/development.txt b/requirements/development.txt index 3073bd9f..4e1abcec 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -1,4 +1,4 @@ -black>=22.1.0 +black==22.12.0 flake8>=4.0.1 notebook==6.4.12 pytest>=6.2.5