├── .gitignore ├── Examples ├── action_commands.py ├── asynchronous_grab.py ├── asynchronous_grab_opencv.py ├── convert_pixel_format.py ├── create_trace_log.py ├── event_handling.py ├── list_cameras.py ├── list_chunk_data.py ├── list_features.py ├── load_save_settings.py ├── multithreading_opencv.py ├── synchronous_grab.py └── user_set.py ├── Install.sh ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── Tests ├── __main__.py ├── basic_tests │ ├── __init__.py │ ├── c_binding_test.py │ ├── frame_test.py │ ├── interface_test.py │ ├── persistable_feature_container_test.py │ ├── transport_layer_test.py │ ├── util_context_decorator_test.py │ ├── util_runtime_type_check_test.py │ ├── util_tracer_test.py │ ├── util_vmb_enum_test.py │ ├── vimbax_common_test.py │ └── vmbsystem_test.py ├── helpers.py ├── real_cam_tests │ ├── __init__.py │ ├── camera_test.py │ ├── chunk_access_test.py │ ├── feature_test.py │ ├── frame_test.py │ ├── local_device_test.py │ ├── persistable_feature_container_test.py │ ├── stream_test.py │ └── vimbax_test.py └── runner.py ├── Uninstall.sh ├── _custom_build └── backend.py ├── pyproject.toml ├── run_tests.py ├── setup.cfg ├── tox.ini └── vmbpy ├── __init__.py ├── c_binding ├── __init__.py ├── vmb_c.py ├── vmb_common.py ├── vmb_image_transform.py └── wrapped_types.py ├── camera.py ├── error.py ├── feature.py ├── featurecontainer.py ├── frame.py ├── interface.py ├── localdevice.py ├── shared.py ├── stream.py ├── transportlayer.py ├── util ├── __init__.py ├── context_decorator.py ├── log.py ├── runtime_type_check.py ├── scoped_log.py ├── tracer.py └── vmb_enum.py └── vmbsystem.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # VSCode 163 | # Ignore local settings or launch configurations 164 | .vscode/ 165 | -------------------------------------------------------------------------------- /Examples/action_commands.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | import sys 29 | from typing import Optional 30 | from vmbpy import * 31 | 32 | 33 | def print_preamble(): 34 | print('/////////////////////////////////////') 35 | print('/// VmbPy Action Commands Example ///') 36 | print('/////////////////////////////////////\n') 37 | 38 | 39 | def print_usage(): 40 | print('Usage:') 41 | print(' python action_commands.py ') 42 | print(' python action_commands.py [/h] [-h]') 43 | print() 44 | print('Parameters:') 45 | print(' camera_id ID of the camera to be used') 46 | print() 47 | 48 | 49 | def abort(reason: str, return_code: int = 1, usage: bool = False): 50 | print(reason + '\n') 51 | 52 | if usage: 53 | print_usage() 54 | 55 | sys.exit(return_code) 56 | 57 | 58 | def parse_args() -> Optional[str]: 59 | args = sys.argv[1:] 60 | argc = len(args) 61 | 62 | cam_id = "" 63 | for arg in args: 64 | if arg in ('/h', '-h'): 65 | print_usage() 66 | sys.exit(0) 67 | elif not cam_id: 68 | cam_id = arg 69 | 70 | if argc > 1: 71 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 72 | 73 | return cam_id if cam_id else None 74 | 75 | 76 | def get_input() -> str: 77 | prompt = 'Press \'a\' to send action command. Press \'q\' to stop example. Enter:' 78 | print(prompt, flush=True) 79 | return input() 80 | 81 | 82 | def get_camera(camera_id: Optional[str]) -> Camera: 83 | with VmbSystem.get_instance() as vmb: 84 | if camera_id: 85 | try: 86 | return vmb.get_camera_by_id(camera_id) 87 | 88 | except VmbCameraError: 89 | abort('Failed to access camera \'{}\'. Abort.'.format(camera_id)) 90 | 91 | else: 92 | cams = vmb.get_all_cameras() 93 | if not cams: 94 | abort('No cameras accessible. Abort.') 95 | 96 | return cams[0] 97 | 98 | 99 | def frame_handler(cam: Camera, stream: Stream, frame: Frame): 100 | if frame.get_status() == FrameStatus.Complete: 101 | print('Frame(ID: {}) has been received.'.format(frame.get_id()), flush=True) 102 | 103 | cam.queue_frame(frame) 104 | 105 | 106 | def main(): 107 | print_preamble() 108 | camera_id = parse_args() 109 | 110 | with VmbSystem.get_instance(): 111 | cam = get_camera(camera_id) 112 | inter = cam.get_interface() 113 | 114 | with cam: 115 | # Prepare camera for ActionCommand - trigger 116 | device_key = 1 117 | group_key = 1 118 | group_mask = 1 119 | 120 | # Try to adjust GeV packet size. This feature is only available for GigE - cameras. 121 | try: 122 | stream = cam.get_streams()[0] 123 | stream.GVSPAdjustPacketSize.run() 124 | while not stream.GVSPAdjustPacketSize.is_done(): 125 | pass 126 | except (AttributeError, VmbFeatureError): 127 | pass 128 | 129 | try: 130 | cam.TriggerSelector.set('FrameStart') 131 | cam.TriggerSource.set('Action0') 132 | cam.TriggerMode.set('On') 133 | cam.ActionDeviceKey.set(device_key) 134 | cam.ActionGroupKey.set(group_key) 135 | cam.ActionGroupMask.set(group_mask) 136 | except (AttributeError, VmbFeatureError): 137 | abort('The selected camera does not seem to support action commands') 138 | 139 | # Enter streaming mode and wait for user input. 140 | try: 141 | cam.start_streaming(frame_handler) 142 | 143 | while True: 144 | ch = get_input() 145 | 146 | if ch == 'q': 147 | break 148 | 149 | elif ch == 'a': 150 | inter.ActionDeviceKey.set(device_key) 151 | inter.ActionGroupKey.set(group_key) 152 | inter.ActionGroupMask.set(group_mask) 153 | inter.ActionCommand.run() 154 | 155 | finally: 156 | cam.stop_streaming() 157 | 158 | 159 | if __name__ == '__main__': 160 | main() 161 | -------------------------------------------------------------------------------- /Examples/asynchronous_grab.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import sys 28 | from typing import Optional, Tuple 29 | 30 | from vmbpy import * 31 | 32 | 33 | def print_preamble(): 34 | print('///////////////////////////////////////') 35 | print('/// VmbPy Asynchronous Grab Example ///') 36 | print('///////////////////////////////////////\n') 37 | 38 | 39 | def print_usage(): 40 | print('Usage:') 41 | print(' python asynchronous_grab.py [/x] [-x] [camera_id]') 42 | print(' python asynchronous_grab.py [/h] [-h]') 43 | print() 44 | print('Parameters:') 45 | print(' /x, -x If set, use AllocAndAnnounce mode of buffer allocation') 46 | print(' camera_id ID of the camera to use (using first camera if not specified)') 47 | print() 48 | 49 | 50 | def abort(reason: str, return_code: int = 1, usage: bool = False): 51 | print(reason + '\n') 52 | 53 | if usage: 54 | print_usage() 55 | 56 | sys.exit(return_code) 57 | 58 | 59 | def parse_args() -> Tuple[Optional[str], AllocationMode]: 60 | args = sys.argv[1:] 61 | argc = len(args) 62 | 63 | allocation_mode = AllocationMode.AnnounceFrame 64 | cam_id = "" 65 | for arg in args: 66 | if arg in ('/h', '-h'): 67 | print_usage() 68 | sys.exit(0) 69 | elif arg in ('/x', '-x'): 70 | allocation_mode = AllocationMode.AllocAndAnnounceFrame 71 | elif not cam_id: 72 | cam_id = arg 73 | 74 | if argc > 2: 75 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 76 | 77 | return (cam_id if cam_id else None, allocation_mode) 78 | 79 | 80 | def get_camera(camera_id: Optional[str]) -> Camera: 81 | with VmbSystem.get_instance() as vmb: 82 | if camera_id: 83 | try: 84 | return vmb.get_camera_by_id(camera_id) 85 | 86 | except VmbCameraError: 87 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 88 | 89 | else: 90 | cams = vmb.get_all_cameras() 91 | if not cams: 92 | abort('No Cameras accessible. Abort.') 93 | 94 | return cams[0] 95 | 96 | 97 | def setup_camera(cam: Camera): 98 | with cam: 99 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 100 | try: 101 | stream = cam.get_streams()[0] 102 | stream.GVSPAdjustPacketSize.run() 103 | 104 | while not stream.GVSPAdjustPacketSize.is_done(): 105 | pass 106 | 107 | except (AttributeError, VmbFeatureError): 108 | pass 109 | 110 | 111 | def frame_handler(cam: Camera, stream: Stream, frame: Frame): 112 | print('{} acquired {}'.format(cam, frame), flush=True) 113 | 114 | cam.queue_frame(frame) 115 | 116 | 117 | def main(): 118 | print_preamble() 119 | cam_id, allocation_mode = parse_args() 120 | 121 | with VmbSystem.get_instance(): 122 | with get_camera(cam_id) as cam: 123 | 124 | setup_camera(cam) 125 | print('Press to stop Frame acquisition.') 126 | 127 | try: 128 | # Start Streaming with a custom a buffer of 10 Frames (defaults to 5) 129 | cam.start_streaming(handler=frame_handler, 130 | buffer_count=10, 131 | allocation_mode=allocation_mode) 132 | input() 133 | 134 | finally: 135 | cam.stop_streaming() 136 | 137 | 138 | if __name__ == '__main__': 139 | main() 140 | -------------------------------------------------------------------------------- /Examples/asynchronous_grab_opencv.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import sys 28 | from typing import Optional 29 | from queue import Queue 30 | 31 | from vmbpy import * 32 | 33 | 34 | # All frames will either be recorded in this format, or transformed to it before being displayed 35 | opencv_display_format = PixelFormat.Bgr8 36 | 37 | 38 | def print_preamble(): 39 | print('///////////////////////////////////////////////////') 40 | print('/// VmbPy Asynchronous Grab with OpenCV Example ///') 41 | print('///////////////////////////////////////////////////\n') 42 | 43 | 44 | def print_usage(): 45 | print('Usage:') 46 | print(' python asynchronous_grab_opencv.py [camera_id]') 47 | print(' python asynchronous_grab_opencv.py [/h] [-h]') 48 | print() 49 | print('Parameters:') 50 | print(' camera_id ID of the camera to use (using first camera if not specified)') 51 | print() 52 | 53 | 54 | def abort(reason: str, return_code: int = 1, usage: bool = False): 55 | print(reason + '\n') 56 | 57 | if usage: 58 | print_usage() 59 | 60 | sys.exit(return_code) 61 | 62 | 63 | def parse_args() -> Optional[str]: 64 | args = sys.argv[1:] 65 | argc = len(args) 66 | 67 | for arg in args: 68 | if arg in ('/h', '-h'): 69 | print_usage() 70 | sys.exit(0) 71 | 72 | if argc > 1: 73 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 74 | 75 | return None if argc == 0 else args[0] 76 | 77 | 78 | def get_camera(camera_id: Optional[str]) -> Camera: 79 | with VmbSystem.get_instance() as vmb: 80 | if camera_id: 81 | try: 82 | return vmb.get_camera_by_id(camera_id) 83 | 84 | except VmbCameraError: 85 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 86 | 87 | else: 88 | cams = vmb.get_all_cameras() 89 | if not cams: 90 | abort('No Cameras accessible. Abort.') 91 | 92 | return cams[0] 93 | 94 | 95 | def setup_camera(cam: Camera): 96 | with cam: 97 | # Enable auto exposure time setting if camera supports it 98 | try: 99 | cam.ExposureAuto.set('Continuous') 100 | 101 | except (AttributeError, VmbFeatureError): 102 | pass 103 | 104 | # Enable white balancing if camera supports it 105 | try: 106 | cam.BalanceWhiteAuto.set('Continuous') 107 | 108 | except (AttributeError, VmbFeatureError): 109 | pass 110 | 111 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 112 | try: 113 | stream = cam.get_streams()[0] 114 | stream.GVSPAdjustPacketSize.run() 115 | while not stream.GVSPAdjustPacketSize.is_done(): 116 | pass 117 | 118 | except (AttributeError, VmbFeatureError): 119 | pass 120 | 121 | 122 | def setup_pixel_format(cam: Camera): 123 | # Query available pixel formats. Prefer color formats over monochrome formats 124 | cam_formats = cam.get_pixel_formats() 125 | cam_color_formats = intersect_pixel_formats(cam_formats, COLOR_PIXEL_FORMATS) 126 | convertible_color_formats = tuple(f for f in cam_color_formats 127 | if opencv_display_format in f.get_convertible_formats()) 128 | 129 | cam_mono_formats = intersect_pixel_formats(cam_formats, MONO_PIXEL_FORMATS) 130 | convertible_mono_formats = tuple(f for f in cam_mono_formats 131 | if opencv_display_format in f.get_convertible_formats()) 132 | 133 | # if OpenCV compatible color format is supported directly, use that 134 | if opencv_display_format in cam_formats: 135 | cam.set_pixel_format(opencv_display_format) 136 | 137 | # else if existing color format can be converted to OpenCV format do that 138 | elif convertible_color_formats: 139 | cam.set_pixel_format(convertible_color_formats[0]) 140 | 141 | # fall back to a mono format that can be converted 142 | elif convertible_mono_formats: 143 | cam.set_pixel_format(convertible_mono_formats[0]) 144 | 145 | else: 146 | abort('Camera does not support an OpenCV compatible format. Abort.') 147 | 148 | 149 | class Handler: 150 | def __init__(self): 151 | self.display_queue = Queue(10) 152 | 153 | def get_image(self): 154 | return self.display_queue.get(True) 155 | 156 | def __call__(self, cam: Camera, stream: Stream, frame: Frame): 157 | if frame.get_status() == FrameStatus.Complete: 158 | print('{} acquired {}'.format(cam, frame), flush=True) 159 | 160 | # Convert frame if it is not already the correct format 161 | if frame.get_pixel_format() == opencv_display_format: 162 | display = frame 163 | else: 164 | # This creates a copy of the frame. The original `frame` object can be requeued 165 | # safely while `display` is used 166 | display = frame.convert_pixel_format(opencv_display_format) 167 | 168 | self.display_queue.put(display.as_opencv_image(), True) 169 | 170 | cam.queue_frame(frame) 171 | 172 | 173 | def main(): 174 | print_preamble() 175 | cam_id = parse_args() 176 | 177 | with VmbSystem.get_instance(): 178 | with get_camera(cam_id) as cam: 179 | # setup general camera settings and the pixel format in which frames are recorded 180 | setup_camera(cam) 181 | setup_pixel_format(cam) 182 | handler = Handler() 183 | 184 | try: 185 | # Start Streaming with a custom a buffer of 10 Frames (defaults to 5) 186 | cam.start_streaming(handler=handler, buffer_count=10) 187 | 188 | msg = 'Stream from \'{}\'. Press to stop stream.' 189 | import cv2 190 | ENTER_KEY_CODE = 13 191 | while True: 192 | key = cv2.waitKey(1) 193 | if key == ENTER_KEY_CODE: 194 | cv2.destroyWindow(msg.format(cam.get_name())) 195 | break 196 | 197 | display = handler.get_image() 198 | cv2.imshow(msg.format(cam.get_name()), display) 199 | 200 | finally: 201 | cam.stop_streaming() 202 | 203 | 204 | if __name__ == '__main__': 205 | main() 206 | -------------------------------------------------------------------------------- /Examples/convert_pixel_format.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import textwrap 4 | from typing import Any, List, Optional 5 | 6 | import vmbpy 7 | 8 | try: 9 | # Check if numpy is available. Not used directly by the example but necessary to create numpy 10 | # arrays that are used as `destination_buffer`s 11 | import numpy as np # noqa: F401 12 | except ImportError: 13 | print('This example requires numpy') 14 | sys.exit(1) 15 | 16 | 17 | def print_preamble(): 18 | print('//////////////////////////////////////////') 19 | print('/// VmbPy convert_pixel_format Example ///') 20 | print('//////////////////////////////////////////\n') 21 | print(flush=True) 22 | 23 | 24 | def abort(reason: str, return_code: int = 1): 25 | print(reason + '\n') 26 | 27 | sys.exit(return_code) 28 | 29 | 30 | def parse_args(): 31 | description = '''\ 32 | VmbPy `Frame.convert_pixel_format` Example 33 | 34 | Records frames in a user selected pixel format and converts them to a different user selected 35 | format. Optionally this transformation can use a pre allocated `destination_buffer` to reduce 36 | possible overhead from memory allocations and garbage collection.''' 37 | 38 | parser = argparse.ArgumentParser(description=textwrap.dedent(description), 39 | formatter_class=argparse.RawDescriptionHelpFormatter) 40 | parser.add_argument('camera_id', default=None, nargs='?', 41 | help='ID of the camera to use (using first camera if not specified)') 42 | parser.add_argument('-d', '--destination_buffer', action='store_true', 43 | help='If this option is given, a `destination_buffer` will be used in ' 44 | 'calls to `convert_pixel_format`') 45 | return parser.parse_args() 46 | 47 | 48 | def get_camera(camera_id: Optional[str]) -> vmbpy.Camera: 49 | with vmbpy.VmbSystem.get_instance() as vmb: 50 | if camera_id: 51 | try: 52 | return vmb.get_camera_by_id(camera_id) 53 | 54 | except vmbpy.VmbCameraError: 55 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 56 | 57 | else: 58 | cams = vmb.get_all_cameras() 59 | if not cams: 60 | abort('No Cameras accessible. Abort.') 61 | 62 | return cams[0] 63 | 64 | 65 | def user_select_from_list(options: List[Any], msg: str = '') -> Any: 66 | if not msg: 67 | msg = 'Please select one of the following:\n' 68 | padding_width = len(str(len(options))) 69 | for idx, element in enumerate(options): 70 | msg += f' [{idx:{padding_width}d}] - {str(element)}\n' 71 | while True: 72 | user_input = input(msg) 73 | try: 74 | selected_index = int(user_input) 75 | except ValueError: 76 | # The user probably typed in the name of the pixel format. Try to find it in the list 77 | try: 78 | selected_index = tuple(map(str, options)).index(user_input) 79 | except ValueError: 80 | print(f'Could not find element "{user_input}" in the list of options.') 81 | continue 82 | try: 83 | selected = options[selected_index] 84 | break 85 | except IndexError: 86 | print(f'Selected index {selected_index} is not valid.') 87 | print(f'Selected option: {str(selected)}') 88 | return selected 89 | 90 | 91 | class FrameProducer: 92 | def __init__(self, cam: vmbpy.Camera, use_destination_buffer): 93 | self.cam = cam 94 | self.use_destination_buffer = use_destination_buffer 95 | # This will later be our user supplied buffer to the convert_pixel_format function. We will 96 | # allocate it appropriately once we receive our first frame from the camera 97 | self.numpy_buffer = None 98 | 99 | def __call__(self, cam: vmbpy.Camera, stream: vmbpy.Stream, frame: vmbpy.Frame): 100 | if self.use_destination_buffer: 101 | if self.numpy_buffer is None: 102 | # Let VmbPy allocate new memory for transformation on the first received frame 103 | # because we do not yet have a prepared buffer. It is recommended to do this by 104 | # performing the conversion once without a destination_buffer and reuse the result 105 | # of that call for future conversions. 106 | converted_frame = frame.convert_pixel_format(self.target_format) 107 | # The memory allocated by this conversion will be reused for all other 108 | # transformations. We save the numpy representation for future use. These steps make 109 | # sure that the numpy buffer has an appropriate data type and shape for our 110 | # conversions. The buffer may only be reused while the shape of the frame and target 111 | # pixel format remains unchanged 112 | self.numpy_buffer = converted_frame.as_numpy_ndarray() 113 | # Use same memory as self.numpy_buffer to store conversion result. Use the `data` field 114 | # of the numpy array as `destination_buffer` parameter 115 | converted_frame = frame.convert_pixel_format(self.target_format, 116 | destination_buffer=self.numpy_buffer.data) 117 | else: 118 | # If no destination_buffer is given, VmbPy will allocate new memory for each conversion. 119 | # This might introduce additional overhead e.g. from the garbage collector 120 | converted_frame = frame.convert_pixel_format(self.target_format) 121 | print(f'Converted frame from {frame.get_pixel_format()} ' 122 | f'to {converted_frame.get_pixel_format()}.\n' 123 | f'Conversion result: {converted_frame}') 124 | # Requeue the original frame for future frame transmissions 125 | stream.queue_frame(frame) 126 | 127 | def setup_camera(self): 128 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 129 | try: 130 | stream = cam.get_streams()[0] 131 | stream.GVSPAdjustPacketSize.run() 132 | 133 | while not stream.GVSPAdjustPacketSize.is_done(): 134 | pass 135 | 136 | except (AttributeError, vmbpy.VmbFeatureError): 137 | pass 138 | 139 | def run(self): 140 | with vmbpy.VmbSystem.get_instance(): 141 | with self.cam: 142 | self.setup_camera() 143 | pixel_formats = self.cam.get_pixel_formats() 144 | record_format = user_select_from_list(pixel_formats, 145 | 'Select PixelFormat that should be used ' 146 | 'to record frames:\n') 147 | self.cam.set_pixel_format(record_format) 148 | convertible_formats = record_format.get_convertible_formats() 149 | self.target_format = user_select_from_list(convertible_formats, 150 | 'Select PixelFormat that the recorded ' 151 | 'frames should be converted to:\n') 152 | try: 153 | self.cam.start_streaming(self) 154 | input('Press to stop Frame acquisition.') 155 | finally: 156 | self.cam.stop_streaming() 157 | 158 | 159 | if __name__ == '__main__': 160 | print_preamble() 161 | args = parse_args() 162 | 163 | with vmbpy.VmbSystem.get_instance() as vmb: 164 | cam = get_camera(args.camera_id) 165 | producer = FrameProducer(cam, args.destination_buffer) 166 | producer.run() 167 | -------------------------------------------------------------------------------- /Examples/create_trace_log.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | import logging # Only needed for manual logging configuration 29 | 30 | from vmbpy import * 31 | 32 | 33 | def main(): 34 | print('//////////////////////////////////////') 35 | print('/// vmbpy Create Trace Log Example ///') 36 | print('//////////////////////////////////////\n') 37 | 38 | # Enable logging mechanism, creating a trace log. The log file is 39 | # stored at the location this script was executed from. 40 | vmb = VmbSystem.get_instance() 41 | vmb.enable_log(LOG_CONFIG_TRACE_FILE_ONLY) 42 | 43 | # While entering this scope, feature, camera and interface discovery occurs. 44 | # All function calls to VmbC are captured in the log file. 45 | with vmb: 46 | pass 47 | 48 | vmb.disable_log() 49 | 50 | 51 | def manual_configuration(): 52 | print('//////////////////////////////////////////////') 53 | print('/// vmbpy Manual Log Configuration Example ///') 54 | print('//////////////////////////////////////////////\n') 55 | 56 | # By default the vmbpy logger instance will not forward its log messages to any handlers. To 57 | # integrate log messages a handler needs to be added to the logger instance. In this example we 58 | # log just the message to the console 59 | logger = Log.get_instance() 60 | # Alternatively the logger instance can also be retrieved by its name: `vmbpyLog` 61 | # logger = logging.getLogger('vmbpyLog') 62 | handler = logging.StreamHandler() 63 | logger.addHandler(handler) 64 | 65 | # By default the level of `logger` is set to the custom level `LogLevel.Trace` defined by VmbPy. 66 | # This will create a lot of output. Adjust as needed for your desired log level 67 | logger.setLevel(logging.INFO) 68 | 69 | # While entering this scope, feature, camera and interface discovery occurs. Only INFO messages 70 | # and higher will be logged since we set the `logger` level to that 71 | vmb = VmbSystem.get_instance() 72 | with vmb: 73 | pass 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | # manual_configuration() 79 | -------------------------------------------------------------------------------- /Examples/event_handling.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | import sys 29 | from typing import Optional 30 | 31 | from vmbpy import * 32 | 33 | 34 | def print_preamble(): 35 | print('////////////////////////////////////') 36 | print('/// VmbPy Event Handling Example ///') 37 | print('////////////////////////////////////\n') 38 | 39 | 40 | def print_usage(): 41 | print('Usage:') 42 | print(' python event_handling.py [camera_id]') 43 | print(' python event_handling.py [/h] [-h]') 44 | print() 45 | print('Parameters:') 46 | print(' camera_id ID of the camera to use (using first camera if not specified)') 47 | print() 48 | 49 | 50 | def abort(reason: str, return_code: int = 1, usage: bool = False): 51 | print(reason + '\n') 52 | 53 | if usage: 54 | print_usage() 55 | 56 | sys.exit(return_code) 57 | 58 | 59 | def parse_args() -> Optional[str]: 60 | args = sys.argv[1:] 61 | argc = len(args) 62 | 63 | for arg in args: 64 | if arg in ('/h', '-h'): 65 | print_usage() 66 | sys.exit(0) 67 | 68 | if argc > 1: 69 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 70 | 71 | return None if argc == 0 else args[0] 72 | 73 | 74 | def get_camera(camera_id: Optional[str]) -> Camera: 75 | with VmbSystem.get_instance() as vmb: 76 | if camera_id: 77 | try: 78 | return vmb.get_camera_by_id(camera_id) 79 | 80 | except VmbCameraError: 81 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 82 | 83 | else: 84 | cams = vmb.get_all_cameras() 85 | if not cams: 86 | abort('No Cameras accessible. Abort.') 87 | 88 | return cams[0] 89 | 90 | 91 | def setup_camera(cam: Camera): 92 | with cam: 93 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 94 | try: 95 | stream = cam.get_streams()[0] 96 | stream.GVSPAdjustPacketSize.run() 97 | while not stream.GVSPAdjustPacketSize.is_done(): 98 | pass 99 | 100 | except (AttributeError, VmbFeatureError): 101 | pass 102 | 103 | 104 | def feature_changed_handler(feature): 105 | msg = 'Feature \'{}\' changed value to \'{}\'' 106 | print(msg.format(str(feature.get_name()), str(feature.get())), flush=True) 107 | 108 | 109 | def frame_callback(cam, stream, frame): 110 | print(frame) 111 | cam.queue_frame(frame) 112 | 113 | 114 | def main(): 115 | print_preamble() 116 | cam_id = parse_args() 117 | 118 | with VmbSystem.get_instance(): 119 | with get_camera(cam_id) as cam: 120 | setup_camera(cam) 121 | 122 | # Disable all events notifications that might be currently enabled 123 | for event in cam.EventSelector.get_available_entries(): 124 | cam.EventSelector.set(event) 125 | cam.EventNotification.set('Off') 126 | 127 | # Enable event notifications on 'AcquisitionStart' 128 | cam.EventSelector.set('AcquisitionStart') 129 | cam.EventNotification.set('On') 130 | 131 | # Register callback for the EventAcquisitionStart feature 132 | cam.EventAcquisitionStart.register_change_handler(feature_changed_handler) 133 | 134 | # Acquire a single Frame. This starts acquisition and triggers the selected event 135 | cam.get_frame() 136 | 137 | 138 | if __name__ == '__main__': 139 | main() 140 | -------------------------------------------------------------------------------- /Examples/list_cameras.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | from vmbpy import * 29 | 30 | 31 | def print_preamble(): 32 | print('//////////////////////////////////') 33 | print('/// VmbPy List Cameras Example ///') 34 | print('//////////////////////////////////\n') 35 | 36 | 37 | def print_camera(cam: Camera): 38 | print('/// Camera Name : {}'.format(cam.get_name())) 39 | print('/// Model Name : {}'.format(cam.get_model())) 40 | print('/// Camera ID : {}'.format(cam.get_id())) 41 | print('/// Serial Number : {}'.format(cam.get_serial())) 42 | print('/// Interface ID : {}\n'.format(cam.get_interface_id())) 43 | 44 | 45 | def main(): 46 | print_preamble() 47 | with VmbSystem.get_instance() as vmb: 48 | cams = vmb.get_all_cameras() 49 | 50 | print('Cameras found: {}'.format(len(cams))) 51 | 52 | for cam in cams: 53 | print_camera(cam) 54 | 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /Examples/list_chunk_data.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import sys 28 | from typing import Optional 29 | 30 | from vmbpy import * 31 | 32 | 33 | def print_preamble(): 34 | print('/////////////////////////////////////') 35 | print('/// VmbPy List Chunk Data Example ///') 36 | print('/////////////////////////////////////\n') 37 | 38 | 39 | def print_usage(): 40 | print('Usage:') 41 | print(' python list_chunk_data.py [camera_id]') 42 | print(' python list_chunk_data.py [/h] [-h]') 43 | print() 44 | print('Parameters:') 45 | print(' camera_id ID of the camera to use (using first camera if not specified)') 46 | print() 47 | 48 | 49 | def abort(reason: str, return_code: int = 1, usage: bool = False): 50 | print(reason + '\n') 51 | 52 | if usage: 53 | print_usage() 54 | 55 | sys.exit(return_code) 56 | 57 | 58 | def parse_args() -> Optional[str]: 59 | args = sys.argv[1:] 60 | argc = len(args) 61 | 62 | for arg in args: 63 | if arg in ('/h', '-h'): 64 | print_usage() 65 | sys.exit(0) 66 | 67 | if argc > 1: 68 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 69 | 70 | return None if argc == 0 else args[0] 71 | 72 | 73 | def get_camera(camera_id: Optional[str]) -> Camera: 74 | with VmbSystem.get_instance() as vmb: 75 | if camera_id: 76 | try: 77 | return vmb.get_camera_by_id(camera_id) 78 | 79 | except VmbCameraError: 80 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 81 | 82 | else: 83 | cams = vmb.get_all_cameras() 84 | if not cams: 85 | abort('No Cameras accessible. Abort.') 86 | 87 | return cams[0] 88 | 89 | 90 | class ChunkExample: 91 | def __init__(self, cam: Camera) -> None: 92 | self.cam = cam 93 | self.enabled_chunk_selectors = [] 94 | 95 | def run(self): 96 | with self.cam: 97 | self.setup_camera() 98 | print('Press to stop Frame acquisition.') 99 | try: 100 | self.cam.start_streaming(handler=self.frame_callback, buffer_count=10) 101 | input() 102 | finally: 103 | self.cam.stop_streaming() 104 | 105 | def setup_camera(self): 106 | with self.cam: 107 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 108 | try: 109 | stream = self.cam.get_streams()[0] 110 | stream.GVSPAdjustPacketSize.run() 111 | while not stream.GVSPAdjustPacketSize.is_done(): 112 | pass 113 | 114 | except (AttributeError, VmbFeatureError): 115 | pass 116 | 117 | try: 118 | # Turn on a selection of Chunk features 119 | self.cam.ChunkModeActive.set(False) 120 | for selector in ('Timestamp', 'Width', 'Height', 'ExposureTime'): 121 | try: 122 | self.cam.ChunkSelector.set(selector) 123 | self.cam.ChunkEnable.set(True) 124 | self.enabled_chunk_selectors.append(selector) 125 | except VmbFeatureError: 126 | print('The device does not support chunk feature "{}". It was not enabled.' 127 | ''.format(selector)) 128 | self.cam.ChunkModeActive.set(True) 129 | except (AttributeError, VmbFeatureError): 130 | abort('Failed to enable Chunk Mode for camera \'{}\'. Abort.' 131 | ''.format(self.cam.get_id())) 132 | 133 | def frame_callback(self, cam: Camera, stream: Stream, frame: Frame): 134 | print('{} acquired {}'.format(cam, frame), flush=True) 135 | if frame.contains_chunk_data(): 136 | frame.access_chunk_data(self.chunk_callback) 137 | else: 138 | print('No Chunk Data present', flush=True) 139 | cam.queue_frame(frame) 140 | 141 | def chunk_callback(self, features: FeatureContainer): 142 | # Print information provided by chunk features that were enabled for this example. More 143 | # features are available (e.g. via features.get_all_features()) 144 | if self.enabled_chunk_selectors: 145 | msg = 'Chunk Data:' 146 | for selector in self.enabled_chunk_selectors: 147 | chunk_feature_name = 'Chunk' + selector 148 | # Access to chunk data is only possible via the passed FeatureContainer instance 149 | chunk_feature = features.get_feature_by_name(chunk_feature_name) 150 | msg += ' {}={}'.format(chunk_feature_name, chunk_feature.get()) 151 | else: 152 | msg = 'No chunk selectors were enabled. No chunk data to show.' 153 | print(msg, flush=True) 154 | 155 | 156 | def main(): 157 | print_preamble() 158 | cam_id = parse_args() 159 | 160 | with VmbSystem.get_instance(): 161 | cam = get_camera(cam_id) 162 | chunk_example = ChunkExample(cam) 163 | chunk_example.run() 164 | 165 | 166 | if __name__ == '__main__': 167 | main() 168 | -------------------------------------------------------------------------------- /Examples/list_features.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import argparse 28 | import sys 29 | 30 | from vmbpy import * 31 | 32 | 33 | def print_preamble(): 34 | print('///////////////////////////////////') 35 | print('/// VmbPy List Features Example ///') 36 | print('///////////////////////////////////\n') 37 | 38 | 39 | def abort(reason: str, return_code: int = 1): 40 | print(reason + '\n') 41 | 42 | sys.exit(return_code) 43 | 44 | 45 | def parse_args() -> argparse.Namespace: 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument('-v', 48 | type=str, 49 | help='The maximum visibility level of features that should be printed ' 50 | '(default = %(default)s)', 51 | # Allow all visibility levels except 'Unknown' 52 | choices=list(map(lambda x: x.name, 53 | filter(lambda x: x != FeatureVisibility.Unknown, 54 | FeatureVisibility))), 55 | default=FeatureVisibility.Guru.name) 56 | group = parser.add_mutually_exclusive_group() 57 | group.add_argument('-t', 58 | type=int, 59 | metavar='TransportLayerIndex', 60 | help='Show transport layer features') 61 | group.add_argument('-i', 62 | type=int, 63 | metavar='InterfaceIndex', 64 | help='Show interface features') 65 | group.add_argument('-c', 66 | type=str, 67 | default='0', 68 | metavar='(CameraIndex | CameraId)', 69 | help='Show the remote device features of the specified camera') 70 | group.add_argument('-l', 71 | type=str, 72 | metavar='(CameraIndex | CameraId)', 73 | help='Show the local device features of the specified camera') 74 | group.add_argument('-s', 75 | type=str, 76 | nargs=2, 77 | metavar=('(CameraIndex | CameraId)', 'StreamIndex'), 78 | help='Show the features of a stream for the specified camera') 79 | 80 | return parser.parse_args() 81 | 82 | 83 | def print_all_features(module: FeatureContainer, max_visibility_level: FeatureVisibility): 84 | for feat in module.get_all_features(): 85 | if feat.get_visibility() <= max_visibility_level: 86 | print_feature(feat) 87 | 88 | 89 | def print_feature(feature: FeatureTypes): 90 | try: 91 | value = feature.get() 92 | 93 | except (AttributeError, VmbFeatureError): 94 | value = None 95 | 96 | print('/// Feature name : {}'.format(feature.get_name())) 97 | print('/// Display name : {}'.format(feature.get_display_name())) 98 | print('/// Tooltip : {}'.format(feature.get_tooltip())) 99 | print('/// Description : {}'.format(feature.get_description())) 100 | print('/// SFNC Namespace : {}'.format(feature.get_sfnc_namespace())) 101 | print('/// Value : {}\n'.format(str(value))) 102 | 103 | 104 | def get_transport_layer(index: int) -> TransportLayer: 105 | with VmbSystem.get_instance() as vmb: 106 | try: 107 | return vmb.get_all_transport_layers()[index] 108 | except IndexError: 109 | abort('Could not find transport layer at index \'{}\'. ' 110 | 'Only found \'{}\' transport layer(s)' 111 | ''.format(index, len(vmb.get_all_transport_layers()))) 112 | 113 | 114 | def get_interface(index: int) -> Interface: 115 | with VmbSystem.get_instance() as vmb: 116 | try: 117 | return vmb.get_all_interfaces()[index] 118 | except IndexError: 119 | abort('Could not find interface at index \'{}\'. Only found \'{}\' interface(s)' 120 | ''.format(index, len(vmb.get_all_interfaces()))) 121 | 122 | 123 | def get_camera(camera_id_or_index: str) -> Camera: 124 | camera_index = None 125 | camera_id = None 126 | try: 127 | camera_index = int(camera_id_or_index) 128 | except ValueError: 129 | # Could not parse `camera_id_or_index` to an integer. It is probably a device ID 130 | camera_id = camera_id_or_index 131 | 132 | with VmbSystem.get_instance() as vmb: 133 | if camera_index is not None: 134 | cams = vmb.get_all_cameras() 135 | if not cams: 136 | abort('No cameras accessible. Abort.') 137 | try: 138 | return cams[camera_index] 139 | except IndexError: 140 | abort('Could not find camera at index \'{}\'. Only found \'{}\' camera(s)' 141 | ''.format(camera_index, len(cams))) 142 | 143 | else: 144 | try: 145 | return vmb.get_camera_by_id(camera_id) 146 | 147 | except VmbCameraError: 148 | abort('Failed to access camera \'{}\'. Abort.'.format(camera_id)) 149 | 150 | 151 | def main(): 152 | print_preamble() 153 | args = parse_args() 154 | visibility_level = FeatureVisibility[args.v] 155 | 156 | with VmbSystem.get_instance(): 157 | if args.t is not None: 158 | tl = get_transport_layer(args.t) 159 | print_all_features(tl, visibility_level) 160 | elif args.i is not None: 161 | inter = get_interface(args.i) 162 | print_all_features(inter, visibility_level) 163 | elif args.l is not None: 164 | cam = get_camera(args.l) 165 | with cam: 166 | local_device = cam.get_local_device() 167 | print_all_features(local_device, visibility_level) 168 | elif args.s is not None: 169 | cam = get_camera(args.s[0]) 170 | with cam: 171 | try: 172 | stream_index = int(args.s[1]) 173 | except ValueError: 174 | abort('Could not parse \'{}\' to a stream index integer'.format(args.s[1])) 175 | try: 176 | stream = cam.get_streams()[stream_index] 177 | print_all_features(stream, visibility_level) 178 | except IndexError: 179 | abort('Could not get stream at index \'{}\'. Camera provides only \'{}\' ' 180 | 'stream(s)'.format(stream_index, len(cam.get_streams()))) 181 | else: 182 | cam = get_camera(args.c) 183 | with cam: 184 | print_all_features(cam, visibility_level) 185 | 186 | 187 | if __name__ == '__main__': 188 | main() 189 | -------------------------------------------------------------------------------- /Examples/load_save_settings.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import sys 28 | from typing import Optional 29 | 30 | from vmbpy import * 31 | 32 | 33 | def print_preamble(): 34 | print('////////////////////////////////////////') 35 | print('/// VmbPy Load Save Settings Example ///') 36 | print('////////////////////////////////////////\n') 37 | 38 | 39 | def print_usage(): 40 | print('Usage:') 41 | print(' python load_save_settings.py [camera_id]') 42 | print(' python load_save_settings.py [/h] [-h]') 43 | print() 44 | print('Parameters:') 45 | print(' camera_id ID of the camera to use (using first camera if not specified)') 46 | print() 47 | 48 | 49 | def abort(reason: str, return_code: int = 1, usage: bool = False): 50 | print(reason + '\n') 51 | 52 | if usage: 53 | print_usage() 54 | 55 | sys.exit(return_code) 56 | 57 | 58 | def parse_args() -> Optional[str]: 59 | args = sys.argv[1:] 60 | argc = len(args) 61 | 62 | for arg in args: 63 | if arg in ('/h', '-h'): 64 | print_usage() 65 | sys.exit(0) 66 | 67 | if argc > 1: 68 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 69 | 70 | return None if argc == 0 else args[0] 71 | 72 | 73 | def get_camera(camera_id: Optional[str]) -> Camera: 74 | with VmbSystem.get_instance() as vmb: 75 | if camera_id: 76 | try: 77 | return vmb.get_camera_by_id(camera_id) 78 | 79 | except VmbCameraError: 80 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 81 | 82 | else: 83 | cams = vmb.get_all_cameras() 84 | if not cams: 85 | abort('No Cameras accessible. Abort.') 86 | 87 | return cams[0] 88 | 89 | 90 | def main(): 91 | print_preamble() 92 | cam_id = parse_args() 93 | 94 | with VmbSystem.get_instance(): 95 | print("--> VmbSystem context has been entered") 96 | 97 | with get_camera(cam_id) as cam: 98 | print("--> Camera has been opened (%s)" % cam.get_id()) 99 | 100 | # Save camera settings to file. 101 | settings_file = '{}_settings.xml'.format(cam.get_id()) 102 | cam.save_settings(settings_file, PersistType.All) 103 | print("--> Feature values have been saved to '%s'" % settings_file) 104 | 105 | # Restore settings to initial value. 106 | try: 107 | cam.UserSetSelector.set('Default') 108 | 109 | except (AttributeError, VmbFeatureError): 110 | abort('Failed to set Feature \'UserSetSelector\'') 111 | 112 | try: 113 | cam.UserSetLoad.run() 114 | print("--> All feature values have been restored to default") 115 | 116 | except (AttributeError, VmbFeatureError): 117 | abort('Failed to run Feature \'UserSetLoad\'') 118 | 119 | # Load camera settings from file. 120 | cam.load_settings(settings_file, PersistType.All) 121 | print("--> Feature values have been loaded from given file '%s'" % settings_file) 122 | 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /Examples/synchronous_grab.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import sys 28 | from typing import Optional 29 | 30 | from vmbpy import * 31 | 32 | 33 | def print_preamble(): 34 | print('//////////////////////////////////////') 35 | print('/// VmbPy Synchronous Grab Example ///') 36 | print('//////////////////////////////////////\n') 37 | 38 | 39 | def print_usage(): 40 | print('Usage:') 41 | print(' python synchronous_grab.py [camera_id]') 42 | print(' python synchronous_grab.py [/h] [-h]') 43 | print() 44 | print('Parameters:') 45 | print(' camera_id ID of the camera to use (using first camera if not specified)') 46 | print() 47 | 48 | 49 | def abort(reason: str, return_code: int = 1, usage: bool = False): 50 | print(reason + '\n') 51 | 52 | if usage: 53 | print_usage() 54 | 55 | sys.exit(return_code) 56 | 57 | 58 | def parse_args() -> Optional[str]: 59 | args = sys.argv[1:] 60 | argc = len(args) 61 | 62 | for arg in args: 63 | if arg in ('/h', '-h'): 64 | print_usage() 65 | sys.exit(0) 66 | 67 | if argc > 1: 68 | abort(reason="Invalid number of arguments. Abort.", return_code=2, usage=True) 69 | 70 | return None if argc == 0 else args[0] 71 | 72 | 73 | def get_camera(camera_id: Optional[str]) -> Camera: 74 | with VmbSystem.get_instance() as vmb: 75 | if camera_id: 76 | try: 77 | return vmb.get_camera_by_id(camera_id) 78 | 79 | except VmbCameraError: 80 | abort('Failed to access Camera \'{}\'. Abort.'.format(camera_id)) 81 | 82 | else: 83 | cams = vmb.get_all_cameras() 84 | if not cams: 85 | abort('No Cameras accessible. Abort.') 86 | 87 | return cams[0] 88 | 89 | 90 | def setup_camera(cam: Camera): 91 | with cam: 92 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 93 | try: 94 | stream = cam.get_streams()[0] 95 | stream.GVSPAdjustPacketSize.run() 96 | while not stream.GVSPAdjustPacketSize.is_done(): 97 | pass 98 | 99 | except (AttributeError, VmbFeatureError): 100 | pass 101 | 102 | 103 | def main(): 104 | print_preamble() 105 | cam_id = parse_args() 106 | 107 | with VmbSystem.get_instance(): 108 | with get_camera(cam_id) as cam: 109 | setup_camera(cam) 110 | 111 | # Acquire 10 frame with a custom timeout (default is 2000ms) per frame acquisition. 112 | for frame in cam.get_frame_generator(limit=10, timeout_ms=3000): 113 | print('Got {}'.format(frame), flush=True) 114 | 115 | 116 | if __name__ == '__main__': 117 | main() 118 | -------------------------------------------------------------------------------- /Install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # BSD 2-Clause License 4 | # 5 | # Copyright (c) 2022, Allied Vision Technologies GmbH 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # 14 | # 2. Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | # global parameters parsed from command line flags 30 | DEBUG=false 31 | 32 | while getopts "d" flag; do 33 | case "${flag}" in 34 | d) DEBUG=true ;; 35 | *) ;; 36 | esac 37 | done 38 | 39 | function get_bool_input() 40 | { 41 | QUESTION=$1 42 | TRUTHY=$2 43 | FALSY=$3 44 | DEFAULT=$4 45 | ANSWER="" 46 | 47 | while [[ "$ANSWER" != "$TRUTHY" ]] && [[ "$ANSWER" != "$FALSY" ]] 48 | do 49 | echo -n "$QUESTION" 50 | read ANSWER 51 | 52 | # Use Default value if it was supplied and the input was empty. 53 | if [[ -z "$ANSWER" ]] && [[ ! -z "$DEFAULT" ]] 54 | then 55 | ANSWER=$DEFAULT 56 | fi 57 | 58 | # Print message if given input is invalid. 59 | if [[ "$ANSWER" != "$TRUTHY" ]] && [[ "$ANSWER" != "$FALSY" ]] 60 | then 61 | echo " Error: Given Input must be either \"$TRUTHY\" or \"$FALSY\". Try again." 62 | fi 63 | done 64 | 65 | # Run test command to set return value for later evaluation. 66 | [[ "$ANSWER" == "$TRUTHY" ]] 67 | } 68 | 69 | function inside_virtual_env 70 | { 71 | if [ -z "$VIRTUAL_ENV" ]; then 72 | echo "false" 73 | else 74 | echo "true" 75 | fi 76 | } 77 | 78 | function get_python_versions 79 | { 80 | DETECTED_PYTHONS=() 81 | 82 | # Check if the script was run from a virtual environment and set search path for binary accordingly 83 | if [ "$(inside_virtual_env)" = true ]; then 84 | if [ "$DEBUG" = true ] ; then 85 | echo "Detected active virtual environment" >&2 86 | fi 87 | SEARCH_PATH="$VIRTUAL_ENV"/bin 88 | else 89 | if [ "$DEBUG" = true ] ; then 90 | echo "No virtual environment detected" >&2 91 | fi 92 | SEARCH_PATH=$(echo "$PATH" | tr ":" " ") 93 | fi 94 | 95 | if [ "$DEBUG" = true ] ; then 96 | echo "Searching for python in $SEARCH_PATH" >&2 97 | fi 98 | 99 | # iterate over all detected python binaries and check if they are viable installations 100 | for P in $(whereis -b -B $SEARCH_PATH -f python | tr " " "\n" | grep "python[[:digit:]]\.[[:digit:]]\.\?[[:digit:]]\?$" | sort -V) 101 | do 102 | # 1) Remove results that are links (venv executables are often links so we allow those) 103 | if [ -L "$P" ] && [ "$(inside_virtual_env)" = false ] 104 | then 105 | if [ "$DEBUG" = true ] ; then 106 | echo "$P was a link" >&2 107 | fi 108 | continue 109 | fi 110 | 111 | # 2) Remove results that are directories 112 | if [ -d "$P" ] 113 | then 114 | if [ "$DEBUG" = true ] ; then 115 | echo "$P was a directory" >&2 116 | fi 117 | continue 118 | fi 119 | 120 | # 3) Remove incompatible versions (<3.7) 121 | # patch is ignored but has to be parsed in case the binary name contains it 122 | FILENAME=$(basename -- "$P") 123 | read -r MAJOR MINOR PATCH < <(echo $FILENAME | tr -dc "0-9." | tr "." " ") 124 | if [ $MAJOR -gt 3 ] || { [ $MAJOR -eq 3 ] && [ $MINOR -ge 7 ]; }; then 125 | : # the interperter is compatible 126 | else 127 | if [ "$DEBUG" = true ] ; then 128 | echo "$P is not compatible. vmbpy requires python >=3.7" >&2 129 | fi 130 | continue 131 | fi 132 | 133 | # 4) Remove results that offer no pip support. 134 | $P -m pip > /dev/null 2>&1 135 | if [ $? -ne 0 ] 136 | then 137 | if [ "$DEBUG" = true ] ; then 138 | echo "$P did not have pip support" >&2 139 | fi 140 | continue 141 | fi 142 | DETECTED_PYTHONS+=("$P") 143 | done 144 | echo "${DETECTED_PYTHONS[@]}" 145 | } 146 | 147 | echo "#########################" 148 | echo "# vmbpy install script. #" 149 | echo "#########################" 150 | 151 | ######################### 152 | # Perform sanity checks # 153 | ######################### 154 | 155 | # root is only required if we are not installing in a virtual environment 156 | if [ $UID -ne 0 ] && [ "$(inside_virtual_env)" = false ] 157 | then 158 | echo "Error: Installation requires root priviliges. Abort." 159 | exit 1 160 | fi 161 | 162 | PWD=$(pwd) 163 | PWD=${PWD##*/} 164 | 165 | if [[ "$PWD" != "VmbPy" ]] 166 | then 167 | echo "Error: Please execute Install.sh within VmbPy directory." 168 | exit 1 169 | fi 170 | 171 | # get path to setup.py file 172 | SOURCEDIR="$(find . -name setup.py -type f -printf '%h' -quit)" 173 | if [ -z "$SOURCEDIR" ] 174 | then 175 | echo "Error: setup.py not found. Abort" 176 | exit 1 177 | fi 178 | 179 | PYTHONS=$(get_python_versions) 180 | 181 | if [ -z "$PYTHONS" ] 182 | then 183 | echo "Error: No compatible Python version with pip support found. Abort." 184 | exit 1 185 | fi 186 | 187 | 188 | ########################################### 189 | # Determine python installation for vmbpy # 190 | ########################################### 191 | 192 | # List all given interpreters and create an Index 193 | echo "The following Python versions with pip support were detected:" 194 | 195 | ITER=0 196 | 197 | for ITEM in ${PYTHONS[@]} 198 | do 199 | echo " $ITER: $ITEM" 200 | LAST=$ITER 201 | ITER=$(expr $ITER + 1) 202 | done 203 | 204 | # Read and verfiy user input 205 | while true 206 | do 207 | echo -n "Enter python version to install vmbpy (0 - $LAST, default: 0): " 208 | read TMP 209 | 210 | if [ -z "$TMP" ] 211 | then 212 | TMP=0 213 | fi 214 | 215 | # Check if Input was a number. If so: assign it. 216 | if [ $TMP -eq $TMP ] 2>/dev/null 217 | then 218 | ITER=$TMP 219 | else 220 | echo " Error: Given input was not a number. Try again." 221 | continue 222 | fi 223 | 224 | # Verify Input range 225 | if [ 0 -le $ITER ] && [ $ITER -le $LAST ] 226 | then 227 | break 228 | 229 | else 230 | echo " Error: Given input is not between 0 and $LAST. Try again." 231 | fi 232 | done 233 | 234 | # Search for selected python interpreter 235 | IDX=0 236 | PYTHON="" 237 | 238 | for ITEM in ${PYTHONS[@]} 239 | do 240 | if [ $IDX -eq $ITER ] 241 | then 242 | PYTHON=$ITEM 243 | break 244 | else 245 | IDX=$(expr $IDX + 1) 246 | fi 247 | done 248 | 249 | echo "Installing vmbpy for $PYTHON" 250 | echo "" 251 | 252 | ################################################## 253 | # Determine installation targets from user input # 254 | ################################################## 255 | TARGET="" 256 | 257 | # Ask for numpy support 258 | get_bool_input "Install vmbpy with numpy support (yes/no, default: yes):" "yes" "no" "yes" 259 | if [ $? -eq 0 ] 260 | then 261 | TARGET="numpy-export" 262 | echo "Installing vmbpy with numpy support." 263 | else 264 | echo "Installing vmbpy without numpy support." 265 | fi 266 | echo "" 267 | 268 | # Ask for OpenCV support 269 | get_bool_input "Install vmbpy with OpenCV support (yes/no, default: yes):" "yes" "no" "yes" 270 | if [ $? -eq 0 ] 271 | then 272 | if [ -z $TARGET ] 273 | then 274 | TARGET="opencv-export" 275 | else 276 | TARGET="$TARGET,opencv-export" 277 | fi 278 | echo "Installing vmbpy with OpenCV support." 279 | else 280 | echo "Installing vmbpy without OpenCV support." 281 | fi 282 | echo "" 283 | 284 | # Execute installation via pip 285 | if [ -z $TARGET ] 286 | then 287 | TARGET="$SOURCEDIR" 288 | else 289 | TARGET="$SOURCEDIR[$TARGET]" 290 | fi 291 | 292 | $PYTHON -m pip install "$TARGET" 293 | 294 | if [ $? -eq 0 ] 295 | then 296 | echo "vmbpy installation successful." 297 | else 298 | echo "Error: vmbpy installation failed. Please check pip output for details." 299 | fi 300 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include vmbpy/c_binding/lib * 2 | 3 | graft Tests 4 | graft Examples 5 | graft _custom_build 6 | global-exclude *.py[cod] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VmbPy [![python version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/) 2 | 3 | Python API of the [Vimba X SDK](https://www.alliedvision.com) 4 | 5 | Vimba X is a fully GenICam compliant SDK and the successor of Vimba. VmbPy is the Python API that is 6 | provided by this SDK. It provides access to the full functionality of Vimba X in a pythonic way, 7 | allowing for rapid development of applications. 8 | 9 | # Installation 10 | 11 | To use VmbPy, Python >= 3.7 is required. A ready-to-install packaged `.whl` file of VmbPy can be 12 | found as part of the Vimba X installation, or be downloaded from our [github release 13 | page](https://github.com/alliedvision/VmbPy/releases). The `.whl` can be installed as usual via the 14 | [`pip install`](https://pip.pypa.io/en/stable/cli/pip_install/) command. 15 | 16 | > [!NOTE] 17 | > Depending on the some systems the command might instead be called `pip3`. Check your systems 18 | > Python documentation for details. 19 | 20 | ## Selecting the correct `.whl` 21 | 22 | When selecting the correct `.whl` file for your project, you have two options. The recommended 23 | approach is to use the `.whl` that includes the VmbC libs, as this provides all the required 24 | libraries to get started with development right away. Note, however, that this `.whl` does not 25 | include any Transport Layers or device drivers, which must be installed separately (for example, by 26 | installing Vimba X). You can identify `.whl`s with included libs by the platform tag that is 27 | included in their filename (for example, `win_amd64`). 28 | 29 | Alternatively, you can use the `.whl` without the included VmbC libs (platform tag `any`), but this 30 | requires a pre-existing Vimba X installation on your system, as VmbPy will attempt to load the 31 | necessary libraries at import time. 32 | 33 | > [!NOTE] 34 | > If a `.whl` with included VmbC libs is used, it is possible that binary dependencies of VmbC need 35 | > to be installed separately. For example on windows the Visual C++ Redistributable is required. 36 | > These dependencies will be automatically installed if Vimba X is installed on the system. 37 | 38 | ## Optional dependencies 39 | 40 | For some functionality of VmbPy optional dependencies (also called "extras") are required. These 41 | provide for example integration into numpy and OpenCV, as well as some additional code analysis 42 | tools that are used in our full test suite. The following extras are defined for VmbPy: 43 | 44 | - numpy: Enables conversion of `VmbPy.Frame` objects to numpy arrays 45 | - opencv: Similar to above but ensures that the numpy arrays are valid OpenCV images 46 | - test: Additional tools such as `flake8`, `mypy`, and `coverage` only necessary for executing 47 | `run_tests.py` 48 | 49 | > [!NOTE] 50 | > Installing these extra dependencies is possible by defining them as part of the installation 51 | > command like so (note the single quotes around the filename and extras): 52 | > ``` 53 | > pip install '/path/to/vmbpy-X.Y.Z-py-none-any.whl[numpy,opencv]' 54 | > ``` 55 | 56 | ### Yocto on NXP i.MX8 and OpenCV 57 | 58 | The GPU in i.MX8 systems requires using the system-wide opencv-python package. When you create the 59 | Python environment, please use the `--system-site-packages` flag to include the system-wide OpenCV 60 | package. 61 | 62 | If you don't set up a separate environment, a warning is shown during the VmbPy installation. You 63 | can ignore this warning. 64 | 65 | # Differences to VimbaPython 66 | 67 | VmbPy is the successor of VimbaPython. As such it shares many similarities, but in some places major 68 | differences exist. An overview of the differences between VimbaPython and VmbPy can be found in the 69 | migration guide, that is part of the Vimba X SDK documentation. 70 | 71 | # Usage 72 | 73 | Below is a minimal example demonstrating how to print all available cameras detected by VmbPy. It 74 | highlights the general usage of VmbPy. More complete code examples can be found in the `Examples` 75 | directory. 76 | 77 | ```python 78 | import vmbpy 79 | 80 | vmb = vmbpy.VmbSystem.get_instance() 81 | with vmb: 82 | cams = vmb.get_all_cameras() 83 | for cam in cams: 84 | print(cam) 85 | ``` 86 | 87 | ## General guidelines 88 | 89 | VmbPy makes use of the Python context manager to perform setup and teardown tasks for the used 90 | object instances. This ensures the correct call order for the underlying VmbC functions and 91 | guarantees, that resources are made available and freed as expected even in cases where errors 92 | occur. One example is shown in the example above, where the context of the `VmbSystem` object must 93 | be entered to start the underlying VmbC API. 94 | 95 | VmbPy is also designed to closely resemble VmbCPP. While VmbPy aims to be as performant as possible, 96 | it might not be fast enough for performance critical applications. In that case the similar 97 | structure of VmbPy and VmbCPP makes it easy to migrate an existing VmbPy code base to VmbCPP. 98 | 99 | ## Configuration of the underlying VmbC API 100 | 101 | The underlying VmbC API used by VmbPy can be configured in various ways to suit your development 102 | needs. Multiple configuration options are available, including the ability to enable logging of VmbC 103 | calls and customize the loading behavior of transport layers. Configuration is achieved through the 104 | `VmbC.xml` file, which by default loads Transport Layers from the `GENICAM_GENTL64_PATH` environment 105 | variable. The location of this configuration file depends on the type of `.whl` installed: if you're 106 | using the `.whl` with included libs, it can be found in `/site-packages/vmbpy/c_binding/lib`. If 107 | you're using the `.whl` without included libs, it is located in the `/api/bin` directory of your 108 | Vimba X installation. 109 | 110 | Alternatively it is also possible for the user to provide a `VmbC.xml` configuration with the help 111 | of `VmbSystem.set_path_configuration`. 112 | 113 | ## Running the test suite 114 | 115 | VmbPy provides a number of unittest as part of the [Github 116 | repository](https://github.com/alliedvision/VmbPy). The test suite can be run in two ways. Either by 117 | using the test discovery mechanic of Python's `unittest` module, or via the provided `run_tests.py`. 118 | 119 | ### Unittest discovery 120 | 121 | Python's unittest module can be used to discover the test cases of VimbaPython automatically. This 122 | can be useful as it provides good integration into third party tools like test explorers. To execute 123 | the entire test suite the following command can be run inside the `Tests` directory of this 124 | repository: 125 | 126 | ``` 127 | python -m unittest discover -v -p *_test.py 128 | ``` 129 | 130 | This will open the first camera, that Vimba detects on the system. If multiple cameras are connected 131 | to the system, an unexpected device may be selected. In this situation it is recommended to specify 132 | the ID of the device that should be used for test case execution. This is done by setting the 133 | environment variable `VMBPY_DEVICE_ID`. A convenient way to get the IDs of currently available 134 | devices is running the `list_cameras.py` example. 135 | 136 | Execute entire test suite using camera with ID `DEV_PLACEHOLDER` 137 | 138 |
Windows 139 | 140 | ``` 141 | set VMBPY_DEVICE_ID=DEV_PLACEHOLDER 142 | python -m unittest discover -v -p *_test.py 143 | ``` 144 |
145 | 146 |
Linux 147 | 148 | ``` 149 | export VMBPY_DEVICE_ID=DEV_PLACEHOLDER 150 | python -m unittest discover -v -p *_test.py 151 | ``` 152 |
153 | 154 | ### `run_tests.py` 155 | 156 | The provided helper script `run_tests.py` may also be used to execute the test suite. In addition to 157 | the test cases, it also executes `flake8`, `mypy`, and `coverage`. These additional tools need to be 158 | installed in the used Python environment. For convenience they are grouped as an optional dependency 159 | of VmbPy with the name `test` that can be added to the `pip install` command used to install VmbPy. 160 | 161 | `run_tests.py` provides a helpful command line interface that can be used to configure which tests 162 | to run and how the output should be structured. To get an overview of the available options the 163 | following command can be executed to generate the usage description. 164 | 165 | ``` 166 | python run_tests.py -h 167 | ``` 168 | 169 | # Beta Disclaimer 170 | 171 | Please be aware that all code revisions not explicitly listed in the Github Release section are 172 | considered a **Beta Version**. 173 | 174 | For Beta Versions, the following applies in addition to the BSD 2-Clause License: 175 | 176 | THE SOFTWARE IS PRELIMINARY AND STILL IN TESTING AND VERIFICATION PHASE AND IS PROVIDED ON AN “AS 177 | IS” AND “AS AVAILABLE” BASIS AND IS BELIEVED TO CONTAIN DEFECTS. THE PRIMARY PURPOSE OF THIS EARLY 178 | ACCESS IS TO OBTAIN FEEDBACK ON PERFORMANCE AND THE IDENTIFICATION OF DEFECTS IN THE SOFTWARE, 179 | HARDWARE AND DOCUMENTATION. 180 | -------------------------------------------------------------------------------- /Tests/__main__.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | from runner import main 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /Tests/basic_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alliedvision/VmbPy/a972270be6104e8ab54dea3d3d06195aa97e7864/Tests/basic_tests/__init__.py -------------------------------------------------------------------------------- /Tests/basic_tests/frame_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import copy 28 | import os 29 | import sys 30 | 31 | from vmbpy import * 32 | 33 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 34 | 35 | from helpers import VmbPyTestCase 36 | 37 | 38 | class FrameTest(VmbPyTestCase): 39 | def setUp(self): 40 | pass 41 | 42 | def tearDown(self): 43 | pass 44 | 45 | def test_deepcopy_empty_frame(self): 46 | f = Frame(10, allocation_mode=AllocationMode.AnnounceFrame) 47 | self.assertNoRaise(copy.deepcopy, f) 48 | 49 | def test_convert_pixel_format_empty_frame(self): 50 | f = Frame(10 * 10, allocation_mode=AllocationMode.AnnounceFrame) 51 | f._frame.pixelFormat = PixelFormat.Mono8 52 | f._frame.width = 10 53 | f._frame.height = 10 54 | self.assertNoRaise(f.convert_pixel_format, PixelFormat.Mono8) 55 | self.assertNoRaise(f.convert_pixel_format, PixelFormat.Rgb8) 56 | 57 | def test_as_numpy_array_empty_frame(self): 58 | f = Frame(10 * 10, allocation_mode=AllocationMode.AnnounceFrame) 59 | f._frame.pixelFormat = PixelFormat.Mono8 60 | f._frame.width = 10 61 | f._frame.height = 10 62 | self.assertNoRaise(f.as_numpy_ndarray) 63 | 64 | def test_repr_empty_frame(self): 65 | f = Frame(10, allocation_mode=AllocationMode.AnnounceFrame) 66 | self.assertNoRaise(repr, f) 67 | self.assertNoRaise(repr, f._frame) 68 | -------------------------------------------------------------------------------- /Tests/basic_tests/interface_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import sys 29 | 30 | from vmbpy import * 31 | 32 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 33 | 34 | from helpers import VmbPyTestCase 35 | 36 | 37 | class InterfaceTest(VmbPyTestCase): 38 | def setUp(self): 39 | self.vmb = VmbSystem.get_instance() 40 | self.vmb._startup() 41 | 42 | inters = self.vmb.get_all_interfaces() 43 | 44 | if not inters: 45 | self.vmb._shutdown() 46 | self.skipTest('No Interface available to test against. Abort.') 47 | 48 | def tearDown(self): 49 | self.vmb._shutdown() 50 | 51 | def test_interface_decode_id(self): 52 | # Expectation all interface ids can be decoded in something not '' 53 | for i in self.vmb.get_all_interfaces(): 54 | self.assertNotEqual(i.get_id(), '') 55 | 56 | def test_interface_decode_type(self): 57 | # Expectation all interface types be in transport layer types 58 | # instances of Interface are enumerated by the TransportLayer class. Because of this the 59 | # transport layer decides the type of the interface 60 | for i in self.vmb.get_all_interfaces(): 61 | self.assertIn(i.get_type(), TransportLayerType) 62 | 63 | def test_interface_decode_name(self): 64 | # Expectation all interface names can be decoded in something not '' 65 | for i in self.vmb.get_all_interfaces(): 66 | self.assertNotEqual(i.get_name(), '') 67 | 68 | def test_interface_get_all_features(self): 69 | # Expectation: Call get_all_features raises RuntimeError outside of with 70 | # Inside of with return a non empty set 71 | inter = self.vmb.get_all_interfaces()[0] 72 | self.assertNotEqual(inter.get_all_features(), ()) 73 | 74 | def test_interface_get_features_selected_by(self): 75 | # Expectation: Call get_features_selected_by raises RuntimeError outside of with. 76 | # Inside with it must either return and empty set if the given feature has no selected 77 | # Feature or a set off affected features 78 | inter = self.vmb.get_all_interfaces()[0] 79 | try: 80 | selects_feats = inter.get_feature_by_name('DeviceSelector') 81 | 82 | except VmbFeatureError: 83 | self.skipTest('Test requires Feature \'DeviceSelector\'.') 84 | 85 | try: 86 | not_selects_feats = inter.get_feature_by_name('DeviceCount') 87 | 88 | except VmbFeatureError: 89 | self.skipTest('Test requires Feature \'DeviceCount\'.') 90 | 91 | self.assertTrue(selects_feats.has_selected_features()) 92 | self.assertNotEqual(inter.get_features_selected_by(selects_feats), ()) 93 | 94 | self.assertFalse(not_selects_feats.has_selected_features()) 95 | self.assertEqual(inter.get_features_selected_by(not_selects_feats), ()) 96 | 97 | def test_interface_get_features_by_type(self): 98 | # Expectation: Call get_features_by_type raises RuntimeError outside of with 99 | # Inside of with return a non empty set for IntFeature (DeviceSelector is IntFeature) 100 | inter = self.vmb.get_all_interfaces()[0] 101 | self.assertNotEqual(inter.get_features_by_type(IntFeature), ()) 102 | 103 | def test_interface_get_features_by_category(self): 104 | # Expectation: Call get_features_by_category raises RuntimeError outside of with 105 | # Inside of with return a non empty set for /DeviceEnumeration) 106 | inter = self.vmb.get_all_interfaces()[0] 107 | self.assertNotEqual(inter.get_features_by_category('/DeviceEnumeration'), ()) 108 | 109 | def test_interface_get_feature_by_name(self): 110 | # Expectation: Call get_feature_by_name raises RuntimeError outside of with 111 | # Inside of with return dont raise VmbFeatureError for 'InterfaceID' 112 | # A invalid name must raise VmbFeatureError 113 | inter = self.vmb.get_all_interfaces()[0] 114 | self.assertNoRaise(inter.get_feature_by_name, 'InterfaceID') 115 | self.assertRaises(VmbFeatureError, inter.get_feature_by_name, 'Invalid Name') 116 | 117 | def test_interface_get_transport_layer_type(self): 118 | # Expectation: The transport layer returned by `get_transport_layer` has correct type 119 | for inter in self.vmb.get_all_interfaces(): 120 | self.assertIsInstance(inter.get_transport_layer(), 121 | TransportLayer) 122 | 123 | def test_interface_get_cameras_type(self): 124 | # Expectation: All cameras returned by `get_cameras` have correct type 125 | for inter in self.vmb.get_all_interfaces(): 126 | with self.subTest(f'interface={inter}'): 127 | cameras = inter.get_cameras() 128 | if not cameras: 129 | self.skipTest(f'Could not test because {inter} did not provide any cameras') 130 | for cam in cameras: 131 | self.assertIsInstance(cam, Camera) 132 | 133 | def test_interface_context_sensitivity(self): 134 | # Expectation: Call get_all_features and similar methods outside of VmbSystem context raises 135 | # a RuntimeError and the error message references the VmbSystem context 136 | 137 | # Manually manage VmbSystem context for this test 138 | self.vmb._shutdown() 139 | 140 | inters_and_feats = [] 141 | with self.vmb: 142 | for inter in self.vmb.get_all_interfaces(): 143 | feat = inter.get_all_features()[0] 144 | inters_and_feats.append((inter, feat)) 145 | 146 | for inter, feat in inters_and_feats: 147 | self.assertRaisesRegex(RuntimeError, 148 | 'outside of VmbSystem.* context', 149 | inter.get_all_features) 150 | 151 | self.assertRaisesRegex(RuntimeError, 152 | 'outside of VmbSystem.* context', 153 | inter.get_features_selected_by, 154 | feat) 155 | 156 | self.assertRaisesRegex(RuntimeError, 157 | 'outside of VmbSystem.* context', 158 | inter.get_features_by_type, 159 | IntFeature) 160 | 161 | self.assertRaisesRegex(RuntimeError, 162 | 'outside of VmbSystem.* context', 163 | inter.get_features_by_category, 164 | 'foo') 165 | 166 | self.assertRaisesRegex(RuntimeError, 167 | 'outside of VmbSystem.* context', 168 | inter.get_feature_by_name, 169 | 'foo') 170 | 171 | # Start VmbSystem again so tearDown method works as expected 172 | self.vmb._startup() 173 | -------------------------------------------------------------------------------- /Tests/basic_tests/persistable_feature_container_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import sys 29 | import tempfile 30 | 31 | from vmbpy import * 32 | 33 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 34 | 35 | from helpers import VmbPyTestCase 36 | 37 | 38 | class PersistableFeatureContainerTest(VmbPyTestCase): 39 | @classmethod 40 | def setUpClass(cls) -> None: 41 | super().setUpClass() 42 | cls.vmb = VmbSystem.get_instance() 43 | cls.vmb._startup() 44 | 45 | @classmethod 46 | def tearDownClass(cls) -> None: 47 | cls.vmb._shutdown() 48 | super().tearDownClass() 49 | 50 | def test_transport_layer_save_load(self): 51 | # Expectation: Settings can be saved to a file and loaded from it. 52 | for tl in self.vmb.get_all_transport_layers(): 53 | with self.subTest(f'transport_layer={str(tl)}'): 54 | fname = 'transport_layer.xml' 55 | tl.save_settings(fname) 56 | # Room for improvement: Unfortunately there is no generic writeable feature that 57 | # every interface supports that we can modify here to check that loading the 58 | # settings resets the feature to the original value. So we just load again and if no 59 | # errors occur assume the test to be passed. 60 | tl.load_settings(fname) 61 | 62 | os.remove(fname) 63 | 64 | def test_interface_save_load(self): 65 | # Expectation: Settings can be saved to a file and loaded from it. 66 | for inter in self.vmb.get_all_interfaces(): 67 | with self.subTest(f'interface={str(inter)}'): 68 | fname = 'interface.xml' 69 | inter.save_settings(fname) 70 | # Room for improvement: Unfortunately there is no generic writeable feature that 71 | # every interface supports that we can modify here to check that loading the 72 | # settings resets the feature to the original value. So we just load again and if no 73 | # errors occur assume the test to be passed. 74 | inter.load_settings(fname) 75 | 76 | os.remove(fname) 77 | 78 | def test_save_settings_type_error(self): 79 | # Expectation: Using parameters with incorrect type raises a TypeError 80 | # Use a transport layer to access save settings. Precise class does not matter. It only 81 | # needs to provide access to the method 82 | tl = self.vmb.get_all_transport_layers()[0] 83 | self.assertRaises(TypeError, tl.save_settings, 123) 84 | self.assertRaises(TypeError, tl.save_settings, 'foo.xml', 123) 85 | self.assertRaises(TypeError, tl.save_settings, 'foo.xml', PersistType.All, 123) 86 | self.assertRaises(TypeError, tl.save_settings, 'foo.xml', PersistType.All, 87 | ModulePersistFlags.None_, 'foo') 88 | 89 | def test_load_settings_type_error(self): 90 | # Expectation: Using parameters with incorrect type raises a TypeError 91 | # Use a transport layer to access save settings. Precise class does not matter. It only 92 | # needs to provide access to the method 93 | tl = self.vmb.get_all_transport_layers()[0] 94 | self.assertRaises(TypeError, tl.load_settings, 123) 95 | self.assertRaises(TypeError, tl.load_settings, 'foo.xml', 123) 96 | self.assertRaises(TypeError, tl.load_settings, 'foo.xml', PersistType.All, 123) 97 | self.assertRaises(TypeError, tl.load_settings, 'foo.xml', PersistType.All, 98 | ModulePersistFlags.None_, 'foo') 99 | 100 | def test_save_settings_verify_path(self): 101 | # Expectation: Valid files end with .xml and can be either a absolute path or relative 102 | # path to the given File. Everything else is a ValueError. 103 | 104 | # create a temporary directory to test relative paths with subdirs 105 | with tempfile.TemporaryDirectory() as tmpdir: 106 | valid_paths = ( 107 | 'valid1_save.xml', 108 | os.path.join('.', 'valid2_save.xml'), 109 | os.path.join(tmpdir, 'valid3_save.xml'), 110 | os.path.join(os.path.dirname(os.path.abspath(__file__)), 'valid4_save.xml'), 111 | ) 112 | tl = self.vmb.get_all_transport_layers()[0] 113 | self.assertRaises(ValueError, tl.save_settings, 'inval.xm') 114 | 115 | for path in valid_paths: 116 | self.assertNoRaise(tl.save_settings, path) 117 | os.remove(path) 118 | 119 | def test_load_settings_verify_path(self): 120 | # Expectation: Valid files end with .xml and must exist before before any execution. 121 | 122 | # create a temporary directory to test relative paths with subdirs 123 | with tempfile.TemporaryDirectory() as tmpdir: 124 | valid_paths = ( 125 | 'valid1_load.xml', 126 | os.path.join('.', 'valid2_load.xml'), 127 | os.path.join(tmpdir, 'valid3_load.xml'), 128 | os.path.join(os.path.dirname(os.path.abspath(__file__)), 'valid4_load.xml'), 129 | ) 130 | tl = self.vmb.get_all_transport_layers()[0] 131 | self.assertRaises(ValueError, tl.load_settings, 'inval.xm', PersistType.All) 132 | 133 | for path in valid_paths: 134 | self.assertRaises(ValueError, tl.load_settings, path, PersistType.All) 135 | 136 | for path in valid_paths: 137 | tl.save_settings(path, PersistType.All) 138 | 139 | self.assertNoRaise(tl.load_settings, path, PersistType.All) 140 | os.remove(path) 141 | 142 | def test_load_settings_api_context_sensitivity_inside_context(self): 143 | # Expectation: Calling load_settings outside of VmbSystem context raises a RuntimeError and 144 | # the error message references the VmbSystem context 145 | 146 | tl = self.vmb.get_all_transport_layers()[0] 147 | inter = self.vmb.get_all_interfaces()[0] 148 | 149 | # Manually manage VmbSystem context for this test 150 | self.vmb._shutdown() 151 | 152 | # Make sure that the error message for these classes references their parent scope 153 | self.assertRaisesRegex(RuntimeError, 154 | 'outside of VmbSystem.* context', 155 | tl.load_settings) 156 | self.assertRaisesRegex(RuntimeError, 157 | 'outside of VmbSystem.* context', 158 | inter.load_settings) 159 | 160 | # Start VmbSystem again so tearDown method works as expected 161 | self.vmb._startup() 162 | 163 | def test_save_settings_api_context_sensitivity_inside_context(self): 164 | # Expectation: Calling save_settings outside of VmbSystem context raises a RuntimeError and 165 | # the error message references the VmbSystem context 166 | 167 | tl = self.vmb.get_all_transport_layers()[0] 168 | inter = self.vmb.get_all_interfaces()[0] 169 | 170 | # Manually manage VmbSystem context for this test 171 | self.vmb._shutdown() 172 | 173 | # Make sure that the error message for these classes references their parent scope 174 | self.assertRaisesRegex(RuntimeError, 175 | 'outside of VmbSystem.* context', 176 | tl.save_settings) 177 | self.assertRaisesRegex(RuntimeError, 178 | 'outside of VmbSystem.* context', 179 | inter.save_settings) 180 | 181 | # Start VmbSystem again so tearDown method works as expected 182 | self.vmb._startup() 183 | -------------------------------------------------------------------------------- /Tests/basic_tests/util_context_decorator_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import sys 29 | 30 | from vmbpy.util import * 31 | 32 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 33 | 34 | from helpers import VmbPyTestCase 35 | 36 | 37 | class TestObj: 38 | @LeaveContextOnCall() 39 | def __init__(self): 40 | pass 41 | 42 | @EnterContextOnCall() 43 | def __enter__(self): 44 | pass 45 | 46 | @LeaveContextOnCall() 47 | def __exit__(self, _1, _2, _3): 48 | pass 49 | 50 | @RaiseIfOutsideContext() 51 | def works_inside_context(self): 52 | pass 53 | 54 | @RaiseIfInsideContext() 55 | def works_outside_context(self): 56 | pass 57 | 58 | 59 | class ContextDecoratorTest(VmbPyTestCase): 60 | def setUp(self): 61 | self.test_obj = TestObj() 62 | 63 | def tearDown(self): 64 | pass 65 | 66 | def test_raise_if_inside_context(self): 67 | # Expectation: a decorated method must raise a RuntimeError if a 68 | # Decorated function is called within a with context and 69 | # run properly outside of the context. 70 | 71 | self.assertNoRaise(self.test_obj.works_outside_context) 72 | 73 | with self.test_obj: 74 | self.assertRaises(RuntimeError, self.test_obj.works_outside_context) 75 | 76 | self.assertNoRaise(self.test_obj.works_outside_context) 77 | 78 | def test_raise_if_outside_context(self): 79 | # Expectation: a decorated method must raise a RuntimeError if a 80 | # Decorated function is called outside a with context and 81 | # run properly inside of the context. 82 | 83 | self.assertRaises(RuntimeError, self.test_obj.works_inside_context) 84 | 85 | with self.test_obj: 86 | self.assertNoRaise(self.test_obj.works_inside_context) 87 | 88 | self.assertRaises(RuntimeError, self.test_obj.works_inside_context) 89 | -------------------------------------------------------------------------------- /Tests/basic_tests/util_tracer_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import sys 29 | 30 | from vmbpy.util import * 31 | from vmbpy.util.tracer import _Tracer 32 | 33 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 34 | 35 | from helpers import VmbPyTestCase 36 | 37 | 38 | class TracerTest(VmbPyTestCase): 39 | def setUp(self): 40 | self.log = Log.get_instance() 41 | 42 | self.log.enable(LOG_CONFIG_CRITICAL_CONSOLE_ONLY) 43 | 44 | def tearDown(self): 45 | self.log.disable() 46 | 47 | def test_trace_normal_exit(self): 48 | # Expectation: Must not throw on call normal func. 49 | # Each traced call must add two Log entries: 50 | 51 | @TraceEnable() 52 | def test_func(arg): 53 | return str(arg) 54 | 55 | with self.assertLogs(self.log, level=LogLevel.Trace) as logs: 56 | self.assertEqual(test_func(1), '1') 57 | self.assertEqual(len(logs.records), 2) 58 | 59 | self.assertEqual(test_func('test'), 'test') 60 | self.assertEqual(len(logs.records), 4) 61 | 62 | self.assertEqual(test_func(2.0), '2.0') 63 | self.assertEqual(len(logs.records), 6) 64 | 65 | def test_trace_raised_exit(self): 66 | # Expectation: Throws internally thrown exception and adds two log entries 67 | # Each traced call must add two Log entries: 68 | 69 | @TraceEnable() 70 | def test_func(arg): 71 | raise TypeError('my error') 72 | 73 | with self.assertLogs(self.log, level=LogLevel.Trace) as logs: 74 | self.assertRaises(TypeError, test_func, 1) 75 | self.assertEqual(len(logs.records), 2) 76 | 77 | self.assertRaises(TypeError, test_func, 'test') 78 | self.assertEqual(len(logs.records), 4) 79 | 80 | self.assertRaises(TypeError, test_func, 2.0) 81 | self.assertEqual(len(logs.records), 6) 82 | 83 | def test_trace_function(self): 84 | # Expectation: Normal functions must be traceable 85 | @TraceEnable() 86 | def test_func(): 87 | pass 88 | 89 | with self.assertLogs(self.log, level=LogLevel.Trace) as logs: 90 | test_func() 91 | self.assertEqual(len(logs.records), 2) 92 | 93 | test_func() 94 | self.assertEqual(len(logs.records), 4) 95 | 96 | test_func() 97 | self.assertEqual(len(logs.records), 6) 98 | 99 | def test_trace_lambda(self): 100 | # Expectation: Lambdas must be traceable 101 | 102 | test_lambda = TraceEnable()(lambda: 0) 103 | 104 | with self.assertLogs(self.log, level=LogLevel.Trace) as logs: 105 | test_lambda() 106 | self.assertEqual(len(logs.records), 2) 107 | 108 | test_lambda() 109 | self.assertEqual(len(logs.records), 4) 110 | 111 | test_lambda() 112 | self.assertEqual(len(logs.records), 6) 113 | 114 | def test_trace_object(self): 115 | # Expectation: Objects must be traceable including constructors. 116 | class TestObj: 117 | @TraceEnable() 118 | def __init__(self, arg): 119 | self.arg = arg 120 | 121 | @TraceEnable() 122 | def __str__(self): 123 | return 'TestObj({})'.format(str(self.arg)) 124 | 125 | @TraceEnable() 126 | def __repr__(self): 127 | return 'TestObj({})'.format(repr(self.arg)) 128 | 129 | @TraceEnable() 130 | def __call__(self): 131 | pass 132 | 133 | with self.assertLogs(self.log, level=LogLevel.Trace) as logs: 134 | test_obj = TestObj('test') 135 | self.assertEqual(len(logs.records), 2) 136 | 137 | str(test_obj) 138 | self.assertEqual(len(logs.records), 4) 139 | 140 | repr(test_obj) 141 | self.assertEqual(len(logs.records), 6) 142 | 143 | test_obj() 144 | self.assertEqual(len(logs.records), 8) 145 | 146 | def test_tracer_is_log_enabled(self): 147 | # Expectation: The `_Tracer` class correctly determines if logging is enabled or disabled. 148 | # This reduces overhead by not attempting to create trace entries if logging has been 149 | # completely disabled 150 | self.assertTrue(_Tracer.is_log_enabled()) # Logging is enabled in `setUp` of the test class 151 | self.log.disable() 152 | self.assertFalse(_Tracer.is_log_enabled()) 153 | -------------------------------------------------------------------------------- /Tests/basic_tests/util_vmb_enum_test.py: -------------------------------------------------------------------------------- 1 | from helpers import VmbPyTestCase 2 | import os 3 | import sys 4 | 5 | from vmbpy import * 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 8 | 9 | 10 | class EnumTest(VmbPyTestCase): 11 | def test_string_representation_includes_entry_name(self): 12 | # Expectation: Converting an enum to a string includes the name of the entry in the 13 | # generated string. 14 | enums_to_test = (AccessMode, 15 | AllocationMode, 16 | CameraEvent, 17 | Debayer, 18 | FeatureVisibility, 19 | FrameStatus, 20 | InterfaceEvent, 21 | LogLevel, 22 | ModulePersistFlags, 23 | PersistType, 24 | PixelFormat, 25 | TransportLayerType 26 | ) 27 | for enum in enums_to_test: 28 | for entry in enum: 29 | print(entry) 30 | self.assertIn(entry._name_, str(entry)) 31 | -------------------------------------------------------------------------------- /Tests/basic_tests/vimbax_common_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import sys 29 | 30 | from vmbpy.c_binding import _select_vimbax_home 31 | from vmbpy.error import VmbSystemError 32 | 33 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 34 | 35 | from helpers import VmbPyTestCase 36 | 37 | 38 | class RankVimbaXHomeCandidatesTest(VmbPyTestCase): 39 | def setUp(self): 40 | pass 41 | 42 | def tearDown(self): 43 | pass 44 | 45 | def test_empty_gentl_path(self): 46 | candidates = [] 47 | with self.assertRaises(VmbSystemError): 48 | _select_vimbax_home(candidates) 49 | 50 | def test_empty_string(self): 51 | candidates = [''] 52 | with self.assertRaises(VmbSystemError): 53 | _select_vimbax_home(candidates) 54 | 55 | def test_single_bad_vimbax_home_candidate(self): 56 | candidates = ['/some/path'] 57 | with self.assertRaises(VmbSystemError): 58 | _select_vimbax_home(candidates) 59 | 60 | def test_single_good_vimbax_home_candidate(self): 61 | candidates = ['/opt/VimbaX_2022-1'] 62 | expected = '/opt/VimbaX_2022-1' 63 | self.assertEqual(expected, _select_vimbax_home(candidates)) 64 | 65 | def test_presorted_vimbax_home_candidates(self): 66 | candidates = ['/home/username/VimbaX_2022-1', '/opt/some/other/gentl/provider'] 67 | expected = '/home/username/VimbaX_2022-1' 68 | self.assertEqual(expected, _select_vimbax_home(candidates)) 69 | 70 | def test_unsorted_vimbax_home_candidates(self): 71 | candidates = ['/opt/some/other/gentl/provider', '/home/username/VimbaX_2022-1'] 72 | expected = '/home/username/VimbaX_2022-1' 73 | self.assertEqual(expected, _select_vimbax_home(candidates)) 74 | 75 | def test_many_vimbax_home_candidates(self): 76 | candidates = ['/some/random/path', 77 | '/opt/some/gentl/provider', 78 | '/opt/VimbaX_2022-1', # This should be selected 79 | '/opt/another/gentl/provider', 80 | '/another/incorrect/path'] 81 | expected = '/opt/VimbaX_2022-1' 82 | self.assertEqual(expected, _select_vimbax_home(candidates)) 83 | 84 | def test_vimbax_and_vimba_installation(self): 85 | candidates = ['/opt/Vimba_4_0', # Installation of Allied Vision Vimba 86 | '/opt/VimbaX_2022-1'] # Installation of VimbaX. This should be selected 87 | expected = '/opt/VimbaX_2022-1' 88 | self.assertEqual(expected, _select_vimbax_home(candidates)) 89 | 90 | def test_multiple_vimbax_home_directories(self): 91 | # If multiple VIMBAX_HOME directories are found an error should be raised 92 | candidates = ['/some/random/path', 93 | '/opt/some/gentl/provider', 94 | '/opt/VimbaX_2022-1', # first installation 95 | '/home/username/VimbaX_2023-1', # second installation 96 | '/opt/another/gentl/provider', 97 | '/another/incorrect/path'] 98 | with self.assertRaises(VmbSystemError): 99 | _select_vimbax_home(candidates) 100 | -------------------------------------------------------------------------------- /Tests/helpers.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import unittest 29 | import warnings 30 | 31 | import vmbpy 32 | 33 | 34 | class VmbPyTestCase(unittest.TestCase): 35 | """ 36 | Class that represents a test case for vmbpy. 37 | 38 | Adds the static functions `get_test_camera_id` and `set_test_camera_id` to simplify opening the 39 | appropriate device for testing. 40 | """ 41 | # Initial guess for test cam id is reading appropriate environment variable. If the user 42 | # supplies a value after importing this helper class it will overwrite that initial guess. If 43 | # the environment variable is empty and the user does not supply a device ID before `setUpClass` 44 | # is called, this class will try to detect available cameras and choose the first one. 45 | test_cam_id = os.getenv('VMBPY_DEVICE_ID', '') 46 | 47 | @classmethod 48 | def setUpClass(cls): 49 | with vmbpy.VmbSystem.get_instance() as vmb: 50 | if not VmbPyTestCase.get_test_camera_id(): 51 | try: 52 | VmbPyTestCase.set_test_camera_id(vmb.get_all_cameras()[0].get_id()) 53 | except IndexError: 54 | # no cameras found by VmbC. Leave test_cam_id empty. This will cause tests 55 | # using a real camera to fail 56 | VmbPyTestCase.set_test_camera_id('') 57 | 58 | try: 59 | # Try to adjust GeV packet size. This Feature is only available for GigE - Cameras. 60 | cam = vmb.get_camera_by_id(VmbPyTestCase.get_test_camera_id()) 61 | with cam: 62 | try: 63 | stream = cam.get_streams()[0] 64 | stream.GVSPAdjustPacketSize.run() 65 | 66 | while not stream.GVSPAdjustPacketSize.is_done(): 67 | pass 68 | 69 | except (AttributeError, vmbpy.VmbFeatureError): 70 | pass 71 | 72 | except vmbpy.VmbCameraError: 73 | pass 74 | 75 | print(f'Executing tests in class "{cls.__name__}" ' 76 | f'with camera ID "{VmbPyTestCase.get_test_camera_id()}"', flush=True) 77 | 78 | def assertNoRaise(self, func, *args, **kwargs): 79 | try: 80 | func(*args, **kwargs) 81 | 82 | except BaseException as e: 83 | self.fail('Function raised: {}'.format(e)) 84 | 85 | @staticmethod 86 | def get_test_camera_id() -> str: 87 | return VmbPyTestCase.test_cam_id 88 | 89 | @staticmethod 90 | def set_test_camera_id(test_cam_id): 91 | VmbPyTestCase.test_cam_id = test_cam_id 92 | 93 | 94 | def calculate_acquisition_time(cam: vmbpy.Camera, num_frames: int) -> float: 95 | """ 96 | Calculate how many seconds it takes to record `num_frames` from `cam` in current configuration. 97 | 98 | WARNING: The cams context must already be entered as this function tries to access camera 99 | features! 100 | """ 101 | fps = cam.get_feature_by_name('AcquisitionFrameRate').get() 102 | if fps > 0: 103 | return num_frames / fps 104 | warnings.warn("Failed to get feature 'AcquisitionFrameRate'") 105 | return num_frames * 2.0 # set timeout to 2s. per frame 106 | 107 | 108 | def reset_default_user_set(cam_id: str) -> None: 109 | try: 110 | with vmbpy.VmbSystem.get_instance() as vmb: 111 | cam = vmb.get_camera_by_id(cam_id) 112 | with cam: 113 | try: 114 | cam.get_feature_by_name('UserSetDefault').set('Default') 115 | except vmbpy.VmbFeatureError: 116 | try: 117 | cam.get_feature_by_name('UserSetDefaultSelector').set('Default') 118 | except vmbpy.VmbFeatureError: 119 | warnings.warn('Failed to reset default user set') 120 | except vmbpy.VmbCameraError: 121 | warnings.warn('Camera could not be found to reset the default user set') 122 | 123 | 124 | def load_default_user_set(cam_id: str) -> None: 125 | try: 126 | with vmbpy.VmbSystem.get_instance() as vmb: 127 | cam = vmb.get_camera_by_id(cam_id) 128 | with cam: 129 | try: 130 | cam.get_feature_by_name('UserSetSelector').set('Default') 131 | cam.get_feature_by_name('UserSetLoad').run() 132 | except vmbpy.VmbFeatureError: 133 | warnings.warn('Failed to load default user set') 134 | except vmbpy.VmbCameraError: 135 | warnings.warn('Camera could not be found to load default user set') 136 | 137 | 138 | def _clamp(n, smallest, largest): 139 | return max(smallest, min(largest, n)) 140 | 141 | 142 | def set_throughput_to_fraction(cam: vmbpy.Camera, fraction: float = 0.75): 143 | """ 144 | Set the DeviceLinkThroughputLimit feature of `cam` to `fraction * max_limit`. 145 | 146 | Note: 147 | The value that is actually set on the feature is clamped to the (nim, max) interval. This 148 | means that if the fraction is set to 0.0, the value will be set to min. If the fraction is 149 | set to 1.1, the value will be set to max. 150 | """ 151 | 152 | try: 153 | (min_limit, max_limit) = cam.DeviceLinkThroughputLimit.get_range() 154 | new_value = _clamp(fraction*max_limit, min_limit, max_limit) 155 | cam.DeviceLinkThroughputLimit.set(new_value) 156 | except AttributeError as e: 157 | warnings.warn(f'Camera does not have feature DeviceLinkThroughputLimit: {e}') 158 | except Exception as e: 159 | warnings.warn(f'Could not set DeviceLinkThroughputLimit to {new_value}: {e}') 160 | 161 | 162 | def reset_roi(cam: vmbpy.Camera, roi: int = 0): 163 | """ 164 | Set the ROI (width, height) to specified value. 165 | If no 'roi' value specified - the maximum will be set. 166 | 167 | Limiting ROI should reduce the time to acquire and transfer frames, 168 | making the tests independent from the sensor size. 169 | """ 170 | (min_height, max_height) = cam.Height.get_range() 171 | (min_width, max_width) = cam.Width.get_range() 172 | # roi==0 sets to maximum 173 | height = max_height 174 | width = max_width 175 | if roi > 0: 176 | width = _clamp(roi, min_width, max_width) 177 | height = _clamp(roi, min_height, max_height) 178 | if width != roi or height != roi: 179 | warnings.warn(f'ROI set to minimum: ({width}x{height})') 180 | cam.Width.set(width) 181 | cam.Height.set(height) 182 | -------------------------------------------------------------------------------- /Tests/real_cam_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alliedvision/VmbPy/a972270be6104e8ab54dea3d3d06195aa97e7864/Tests/real_cam_tests/__init__.py -------------------------------------------------------------------------------- /Tests/real_cam_tests/local_device_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import sys 29 | 30 | from vmbpy import * 31 | 32 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 33 | 34 | from helpers import VmbPyTestCase 35 | 36 | 37 | class LocalDeviceTest(VmbPyTestCase): 38 | def setUp(self): 39 | self.vmb = VmbSystem.get_instance() 40 | self.vmb._startup() 41 | 42 | try: 43 | self.cam = self.vmb.get_camera_by_id(self.get_test_camera_id()) 44 | 45 | except VmbCameraError as e: 46 | self.vmb._shutdown() 47 | raise Exception('Failed to lookup Camera.') from e 48 | 49 | try: 50 | self.cam._open() 51 | self.local_device = self.cam.get_local_device() 52 | except VmbCameraError as e: 53 | self.cam._close() 54 | raise Exception('Failed to open Camera {}.'.format(self.cam)) from e 55 | 56 | def tearDown(self): 57 | # In some test cases the camera might already have been closed. In that case an additional 58 | # call to `cam._close` will result in an error. This can be ignored in our test tearDown. 59 | try: 60 | self.cam._close() 61 | except Exception: 62 | pass 63 | self.vmb._shutdown() 64 | 65 | def test_local_device_feature_discovery(self): 66 | # Expectation: Features are detected for the LocalDevice 67 | self.assertNotEqual(self.local_device.get_all_features(), ()) 68 | 69 | def test_local_device_features_category(self): 70 | # Expectation: Getting features by category for an existing category returns a set of 71 | # features, for a non-existent category an empty set is returned 72 | category = self.local_device.get_all_features()[0].get_category() 73 | self.assertNotEqual(self.local_device.get_features_by_category(category), ()) 74 | self.assertEqual(self.local_device.get_features_by_category("Invalid Category"), ()) 75 | 76 | def test_local_device_feature_by_name(self): 77 | # Expectation: A feature can be gotten by name. Invalid feature names raise a 78 | # VmbFeatureError 79 | feat = self.local_device.get_all_features()[0] 80 | self.assertEqual(self.local_device.get_feature_by_name(feat.get_name()), feat) 81 | self.assertRaises(VmbFeatureError, self.local_device.get_feature_by_name, "Invalid Name") 82 | 83 | def test_local_device_features_by_type(self): 84 | # Expectation: Getting features by type returns a set of features for an existing type 85 | type = self.local_device.get_all_features()[0].get_type() 86 | self.assertNotEqual(self.local_device.get_features_by_type(type), ()) 87 | 88 | def test_local_device_features_selected_by(self): 89 | # Expectation: Selected features can be gotten for a feature instance 90 | try: 91 | feat = [f for f in self.local_device.get_all_features() 92 | if f.has_selected_features()].pop() 93 | except IndexError: 94 | self.skipTest('Could not find feature with \'selected features\'') 95 | self.assertNotEqual(self.local_device.get_features_selected_by(feat), ()) 96 | 97 | def test_local_device_context_sensitivity(self): 98 | # Expectation: Call get_all_features outside of Camera context raises a RuntimeError and 99 | # the error message references the Camera context 100 | local_device = self.cam.get_local_device() 101 | feat = local_device.get_all_features()[0] 102 | feat_name = feat.get_name() 103 | 104 | # Ensure that normal calls work while context is still open 105 | self.assertNoRaise(local_device.get_all_features) 106 | self.assertNoRaise(local_device.get_features_selected_by, feat) 107 | self.assertNoRaise(local_device.get_features_by_type, IntFeature) 108 | self.assertNoRaise(local_device.get_features_by_category, 'foo') 109 | self.assertNoRaise(local_device.get_feature_by_name, feat_name) 110 | 111 | # This closes the local device of self.cam implicitly. Alternatively it is possible to call 112 | # local_device._close here if that implicit behavior should not be relied upon 113 | self.cam._close() 114 | self.assertRaisesRegex(RuntimeError, 115 | 'outside of Camera.* context', 116 | local_device.get_all_features) 117 | 118 | self.assertRaisesRegex(RuntimeError, 119 | 'outside of Camera.* context', 120 | local_device.get_features_selected_by, 121 | feat) 122 | 123 | self.assertRaisesRegex(RuntimeError, 124 | 'outside of Camera.* context', 125 | local_device.get_features_by_type, 126 | IntFeature) 127 | 128 | self.assertRaisesRegex(RuntimeError, 129 | 'outside of Camera.* context', 130 | local_device.get_features_by_category, 131 | 'foo') 132 | 133 | self.assertRaisesRegex(RuntimeError, 134 | 'outside of Camera.* context', 135 | local_device.get_feature_by_name, 136 | feat_name) 137 | # open camera context again so tearDown works as expected 138 | self.cam._open() 139 | -------------------------------------------------------------------------------- /Tests/real_cam_tests/vimbax_test.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import ipaddress 28 | import os 29 | import sys 30 | import unittest 31 | 32 | from vmbpy import * 33 | 34 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 35 | 36 | from helpers import VmbPyTestCase 37 | 38 | 39 | class CamVimbaTest(VmbPyTestCase): 40 | def setUp(self): 41 | self.vmb = VmbSystem.get_instance() 42 | 43 | def tearDown(self): 44 | pass 45 | 46 | def test_context_entry_exit(self): 47 | # Expected Behavior: 48 | # On entering the context features, cameras and interfaces shall 49 | # be detected and after leaving the context, everything should be reverted. 50 | 51 | self.assertRaises(RuntimeError, self.vmb.get_all_features) 52 | self.assertRaises(RuntimeError, self.vmb.get_all_interfaces) 53 | self.assertRaises(RuntimeError, self.vmb.get_all_cameras) 54 | 55 | with self.vmb: 56 | self.assertNotEqual(self.vmb.get_all_features(), ()) 57 | self.assertNotEqual(self.vmb.get_all_interfaces(), ()) 58 | self.assertNotEqual(self.vmb.get_all_cameras(), ()) 59 | 60 | self.assertRaises(RuntimeError, self.vmb.get_all_features) 61 | self.assertRaises(RuntimeError, self.vmb.get_all_interfaces) 62 | self.assertRaises(RuntimeError, self.vmb.get_all_cameras) 63 | 64 | def test_get_all_interfaces(self): 65 | # Expected Behavior: get_all_interfaces() must raise an RuntimeError in closed state and 66 | # be non-empty then opened. 67 | self.assertRaises(RuntimeError, self.vmb.get_all_interfaces) 68 | 69 | with self.vmb: 70 | self.assertTrue(self.vmb.get_all_interfaces()) 71 | 72 | def test_get_interface_by_id(self): 73 | # Expected Behavior: All detected Interfaces must be lookup able by their Id. 74 | # If outside of given scope, an error must be returned 75 | with self.vmb: 76 | ids = [inter.get_id() for inter in self.vmb.get_all_interfaces()] 77 | 78 | for id_ in ids: 79 | self.assertNoRaise(self.vmb.get_interface_by_id, id_) 80 | 81 | for id_ in ids: 82 | self.assertRaises(RuntimeError, self.vmb.get_interface_by_id, id_) 83 | 84 | def test_get_all_cameras(self): 85 | # Expected Behavior: get_all_cameras() must only return camera handles on a open camera. 86 | self.assertRaises(RuntimeError, self.vmb.get_all_cameras) 87 | 88 | with self.vmb: 89 | self.assertTrue(self.vmb.get_all_cameras()) 90 | 91 | def test_get_camera_by_id(self): 92 | # Expected Behavior: Lookup of test camera must not fail after system opening 93 | camera_id = self.get_test_camera_id() 94 | 95 | with self.vmb: 96 | self.assertNoRaise(self.vmb.get_camera_by_id, camera_id) 97 | 98 | @unittest.skipIf(VmbPyTestCase.get_test_camera_id().startswith("Sim"), 99 | "Test skipped in simulation mode.") 100 | def test_get_camera_by_ip(self): 101 | # Expected Behavior: get_camera_by_id() must work with a valid ipv4 address. 102 | # A with lookup of an invalid ipv4 address (no Camera attached) 103 | # must raise a VmbCameraError, a lookup with an ipv6 address must raise a 104 | # VmbCameraError in general (VmbC doesn't support ipv6) 105 | with self.vmb: 106 | # Verify that the Test Camera is a GigE - Camera 107 | cam = self.vmb.get_camera_by_id(self.get_test_camera_id()) 108 | inter = self.vmb.get_interface_by_id(cam.get_interface_id()) 109 | 110 | if inter.get_type() != TransportLayerType.GEV: 111 | raise self.skipTest('Test requires GEV Camera.') 112 | 113 | # Lookup test cameras IP address. 114 | with cam: 115 | local_device = cam.get_local_device() 116 | ip_as_number = local_device.get_feature_by_name('GevDeviceIPAddress').get() 117 | 118 | # Verify that lookup with IPv4 Address returns the same Camera Object 119 | ip_addr = str(ipaddress.IPv4Address(ip_as_number)) 120 | self.assertEqual(self.vmb.get_camera_by_id(ip_addr), cam) 121 | 122 | # Verify that a lookup with an invalid IPv4 Address raises a VmbCameraError 123 | ip_addr = str(ipaddress.IPv4Address('127.0.0.1')) 124 | self.assertRaises(VmbCameraError, self.vmb.get_camera_by_id, ip_addr) 125 | 126 | # Verify that a lookup with an IPv6 Address raises a VmbCameraError 127 | ip_addr = str(ipaddress.IPv6Address('FD00::DEAD:BEEF')) 128 | self.assertRaises(VmbCameraError, self.vmb.get_camera_by_id, ip_addr) 129 | 130 | @unittest.skipIf(VmbPyTestCase.get_test_camera_id().startswith("Sim"), 131 | "Test skipped in simulation mode.") 132 | def test_get_camera_by_mac(self): 133 | # Expected Behavior: get_feature_by_id must be usable with a given MAC Address. 134 | with self.vmb: 135 | # Verify that the Test Camera is a GigE - Camera 136 | cam = self.vmb.get_camera_by_id(self.get_test_camera_id()) 137 | inter = self.vmb.get_interface_by_id(cam.get_interface_id()) 138 | 139 | if inter.get_type() != TransportLayerType.GEV: 140 | raise self.skipTest('Test requires GEV Camera.') 141 | 142 | # Lookup test cameras MAC Address. 143 | with cam: 144 | # Construct MAC Address from raw value. 145 | local_device = cam.get_local_device() 146 | mac_as_number = local_device.get_feature_by_name('GevDeviceMACAddress').get() 147 | 148 | mac_as_bytes = mac_as_number.to_bytes(6, byteorder='big') 149 | mac_as_str = ''.join(format(s, '02x') for s in mac_as_bytes).upper() 150 | 151 | # Verify that lookup with MAC Address returns the same Camera Object 152 | self.assertEqual(self.vmb.get_camera_by_id(mac_as_str), cam) 153 | 154 | # Verify that a lookup with an invalid MAC Address raises a VmbCameraError 155 | invalid_mac = 'ffffffff' 156 | self.assertRaises(VmbCameraError, self.vmb.get_camera_by_id, invalid_mac) 157 | -------------------------------------------------------------------------------- /Uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # BSD 2-Clause License 4 | # 5 | # Copyright (c) 2022, Allied Vision Technologies GmbH 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # 14 | # 2. Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | # global parameters parsed from command line flags 30 | DEBUG=false 31 | 32 | while getopts "d" flag; do 33 | case "${flag}" in 34 | d) DEBUG=true ;; 35 | *) ;; 36 | esac 37 | done 38 | 39 | function inside_virtual_env 40 | { 41 | if [ -z "$VIRTUAL_ENV" ]; then 42 | echo "false" 43 | else 44 | echo "true" 45 | fi 46 | } 47 | 48 | 49 | function get_python_versions 50 | { 51 | DETECTED_PYTHONS=() 52 | 53 | # Check if the script was run from a virtual environment and set search path for binary accordingly 54 | if [ "$(inside_virtual_env)" = true ]; then 55 | if [ "$DEBUG" = true ] ; then 56 | echo "Detected active virtual environment" >&2 57 | fi 58 | SEARCH_PATH="$VIRTUAL_ENV"/bin 59 | else 60 | if [ "$DEBUG" = true ] ; then 61 | echo "No virtual environment detected" >&2 62 | fi 63 | SEARCH_PATH=$(echo "$PATH" | tr ":" " ") 64 | fi 65 | 66 | if [ "$DEBUG" = true ] ; then 67 | echo "Searching for python in $SEARCH_PATH" >&2 68 | fi 69 | 70 | # iterate over all detected python binaries and check if they are viable installations 71 | for P in $(whereis -b -B $SEARCH_PATH -f python | tr " " "\n" | grep "python[[:digit:]]\.[[:digit:]]\.\?[[:digit:]]\?$" | sort -V) 72 | do 73 | # 1) Remove results that are links (venv executables are often links so we allow those) 74 | if [ -L "$P" ] && [ "$(inside_virtual_env)" = false ] 75 | then 76 | if [ "$DEBUG" = true ] ; then 77 | echo "$P was a link" >&2 78 | fi 79 | continue 80 | fi 81 | 82 | # 2) Remove results that are directories 83 | if [ -d "$P" ] 84 | then 85 | if [ "$DEBUG" = true ] ; then 86 | echo "$P was a directory" >&2 87 | fi 88 | continue 89 | fi 90 | 91 | # 3) Remove results that offer no pip support. 92 | $P -m pip > /dev/null 2>&1 93 | if [ $? -ne 0 ] 94 | then 95 | if [ "$DEBUG" = true ] ; then 96 | echo "$P did not have pip support" >&2 97 | fi 98 | continue 99 | fi 100 | 101 | # 4) Remove results where vmbpy is not installed 102 | if [ $($P -m pip list --format=columns | grep "vmbpy" | wc -l) -ne 1 ] 103 | then 104 | if [ "$DEBUG" = true ] ; then 105 | echo "$P did not have vmbpy installed" >&2 106 | fi 107 | continue 108 | fi 109 | DETECTED_PYTHONS+=("$P") 110 | done 111 | echo "${DETECTED_PYTHONS[@]}" 112 | } 113 | echo "###########################" 114 | echo "# vmbpy uninstall script. #" 115 | echo "###########################" 116 | 117 | ######################### 118 | # Perform sanity checks # 119 | ######################### 120 | 121 | if [ $UID -ne 0 ] && [ "$(inside_virtual_env)" = false ] 122 | then 123 | echo "Error: Uninstallation requires root privileges. Abort." 124 | exit 1 125 | fi 126 | 127 | PWD=$(pwd) 128 | PWD=${PWD##*/} 129 | 130 | if [[ "$PWD" != "VmbPy" ]] 131 | then 132 | echo "Error: Please execute Uninstall.sh within VmbPy directory." 133 | exit 1 134 | fi 135 | 136 | PYTHONS=$(get_python_versions) 137 | 138 | if [ -z "$PYTHONS" ] 139 | then 140 | echo "Can't remove vmbpy. No installation was found." 141 | exit 0 142 | fi 143 | 144 | ####################################### 145 | # Determine python to uninstall vmbpy # 146 | ####################################### 147 | 148 | # List all given interpreters and create an Index 149 | echo "vmbpy is installed for the following interpreters:" 150 | 151 | ITER=0 152 | 153 | for ITEM in ${PYTHONS[@]} 154 | do 155 | echo " $ITER: $ITEM" 156 | LAST=$ITER 157 | ITER=$(expr $ITER + 1) 158 | done 159 | 160 | # Read and verfiy user input 161 | while true 162 | do 163 | echo -n "Enter python version to uninstall vmbpy (0 - $LAST, all: a, default: a): " 164 | read TMP 165 | 166 | # Set TMP to default value if nothing was entered. 167 | if [ -z $TMP ] 168 | then 169 | TMP="a" 170 | fi 171 | 172 | # Check if Input was "a". If so skip further Input verification. 173 | if [ "$TMP" == "a" ] 174 | then 175 | echo " Removing all installations of vmbpy." 176 | ITER=$TMP 177 | break 178 | 179 | else 180 | # Check if Input was a number. If so: assign it. 181 | if [ $TMP -eq $TMP ] 2>/dev/null 182 | then 183 | ITER=$TMP 184 | 185 | else 186 | echo " Error: Given input was not a number. Try again." 187 | continue 188 | fi 189 | 190 | # Verify Input range 191 | if [ 0 -le $ITER -a $ITER -le $LAST ] 192 | then 193 | break 194 | 195 | else 196 | echo " Error: Given input is not between 0 and $LAST. Try again." 197 | fi 198 | fi 199 | done 200 | 201 | # Search for selected python interpreter 202 | IDX=0 203 | PYTHON="" 204 | 205 | for ITEM in ${PYTHONS[@]} 206 | do 207 | if [ "$ITER" == "a" ] 208 | then 209 | PYTHON=$PYTHONS 210 | break 211 | 212 | elif [ $IDX -eq $ITER ] 213 | then 214 | PYTHON=$ITEM 215 | break 216 | else 217 | IDX=$(expr $IDX + 1) 218 | fi 219 | done 220 | 221 | 222 | # Remove vmbpy via pip 223 | for P in ${PYTHON[@]} 224 | do 225 | echo "" 226 | echo "Remove vmbpy for $P" 227 | 228 | $P -m pip uninstall --yes vmbpy 229 | 230 | if [ $? -eq 0 ] 231 | then 232 | echo "vmbpy removal for $P was successful." 233 | else 234 | echo "Error: vmbpy removal for $P failed. Please check pip output for details." 235 | fi 236 | done 237 | -------------------------------------------------------------------------------- /_custom_build/backend.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from pathlib import Path 3 | from setuptools import build_meta as _orig 4 | from setuptools.build_meta import * 5 | from typing import Optional 6 | 7 | import os 8 | import re 9 | import shutil 10 | import sysconfig 11 | 12 | 13 | def find_vimba_x_home() -> Path: 14 | """ 15 | returns a `Path` instance to the Vimba X Home directory or raises an error if no Vimba X Home 16 | directory could be found 17 | """ 18 | # First choice is always VIMBA_X_HOME environment variable 19 | vimba_x_home = os.getenv('VIMBA_X_HOME') 20 | if vimba_x_home is not None: 21 | return Path(vimba_x_home) 22 | # TODO: Try to find it from GENICAM_GENTL64_PATH 23 | raise FileNotFoundError('Could not find a Vimba X installation to get shared libraries from. ' 24 | 'Install Vimba X or manually set a search path by adding the argument ' 25 | '`--config-setting=--vmb-dir=/path/to/vimbax/binary/directory`') 26 | 27 | 28 | def copy_files(file_paths, target_dir): 29 | """ 30 | Copy all files in `file_paths` to `target_dir` removing any common prefix shared among all files 31 | in `file_paths` 32 | 33 | Returns a list of `Path`s to the files that were copied. 34 | """ 35 | # Find the common path prefix and ensure it ends with a path separator 36 | common_path = os.path.commonpath(file_paths) 37 | if os.path.isdir(common_path): 38 | common_path = os.path.join(common_path, '') 39 | 40 | copied_files = [] 41 | for file_path in file_paths: 42 | # Remove the common prefix 43 | relative_path = os.path.relpath(file_path, common_path) 44 | 45 | # Construct the new file destination 46 | target_path = os.path.join(target_dir, relative_path) 47 | 48 | # Ensure the target directory exists 49 | os.makedirs(os.path.dirname(target_path), exist_ok=True) 50 | 51 | # Copy the file 52 | copied_files.append(Path(shutil.copy2(file_path, target_path))) 53 | return copied_files 54 | 55 | 56 | def delete_files(file_paths): 57 | """ 58 | Delete all files in `file_paths` 59 | 60 | If a path given in `file_paths` points to a directory, that directory is removed. 61 | """ 62 | for file_path in file_paths: 63 | os.remove(file_path) 64 | 65 | # Remove now-empty directories (if any) 66 | for file_path in file_paths: 67 | dir_path = os.path.dirname(file_path) 68 | remove_empty_dirs(dir_path) 69 | 70 | 71 | def remove_empty_dirs(directory): 72 | while True: 73 | try: 74 | if not os.listdir(directory): # Check if the directory is empty 75 | os.rmdir(directory) 76 | # Move to the parent directory 77 | directory = os.path.dirname(directory) 78 | else: 79 | break 80 | except FileNotFoundError: 81 | break 82 | except OSError: 83 | break 84 | 85 | 86 | @contextmanager 87 | def add_vimba_x_libs(search_dir: Path, target_dir: Path): 88 | """ 89 | Context manager that adds the compiled shared libraries from Vimba X that VmbPy requires to the 90 | given `target_dir` while the context is active. When the context is left, all files that were 91 | copied into `target_dir` are removed again. If `target_dir` does not exist it will be created 92 | """ 93 | # TODO: update documentation to inform user about how to change XML config and what the default xml config is and how it might differ to that installed on Windows machines 94 | regex = r'.*(VmbC|VmbImageTransform|_AVT)(.dll|.so|.xml|.dylib)?$' 95 | to_copy = map(Path, filter(re.compile(regex).match, map(str, search_dir.glob('**/*')))) 96 | files_to_copy = [f for f in to_copy if f.is_file()] 97 | if not target_dir.exists(): 98 | target_dir.mkdir(parents=True) 99 | files_to_delete = copy_files(files_to_copy, target_dir) 100 | yield 101 | delete_files(files_to_delete) 102 | 103 | 104 | def build_sdist(sdist_directory, config_settings=None): 105 | if config_settings is None: 106 | config_settings = {} 107 | search_dir = config_settings.get('--vmb-dir') 108 | if search_dir is not None: 109 | search_dir = Path(search_dir) 110 | with add_vimba_x_libs(search_dir, Path(__file__).parent / "../vmbpy/c_binding/lib"): 111 | result = _orig.build_sdist(sdist_directory, config_settings) 112 | else: 113 | result = _orig.build_sdist(sdist_directory, config_settings) 114 | return result 115 | 116 | 117 | def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): 118 | if config_settings is None: 119 | config_settings = {} 120 | # Make sure that the plat-name option is passed down to setuptools correctly so it is added to 121 | # the whl filename. Needed because setuptools does not properly pass `config_settings` through: 122 | # https://github.com/pypa/setuptools/issues/2491 123 | plat_name_arg = '--plat-name' 124 | plat_name_value = config_settings.get(plat_name_arg, None) 125 | search_dir = config_settings.get('--vmb-dir', None) 126 | if plat_name_value is None: 127 | if search_dir is not None: 128 | raise ValueError(f'This build will contain platform specific binaries found in provided ' 129 | f'vmb-dir: {search_dir}. This means a platform-tag must also be ' 130 | f'specified via --plat-name=') 131 | else: 132 | plat_name_value = 'any' 133 | config_settings['--build-option'] = f'{plat_name_arg}={plat_name_value}' 134 | # if the user directly calls pip install, it will not build an sdist first. This means that even 135 | # during wheel build we should make sure the lb directory contains the needed files 136 | if search_dir is not None: 137 | search_dir = Path(search_dir) 138 | with add_vimba_x_libs(search_dir, Path(__file__).parent / "../vmbpy/c_binding/lib"): 139 | result = _orig.build_wheel(wheel_directory, config_settings, metadata_directory) 140 | else: 141 | result = _orig.build_wheel(wheel_directory, config_settings, metadata_directory) 142 | return result 143 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=46"] 3 | build-backend = "backend" 4 | backend-path = ["_custom_build"] 5 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import os 28 | import shutil 29 | import subprocess 30 | 31 | from Tests.runner import Parser 32 | 33 | 34 | def fprint(line): 35 | print(line, flush=True) 36 | 37 | 38 | def stringify_list(l): 39 | list_str = '' 40 | for e in l: 41 | list_str += e + ' ' 42 | 43 | return list_str 44 | 45 | 46 | def static_test(): 47 | fprint('Execute Static Test: flake8') 48 | subprocess.run('flake8 vmbpy', shell=True) 49 | subprocess.run('flake8 Examples --ignore=F405,F403', shell=True) 50 | subprocess.run('flake8 Tests --ignore=F405,F403,E402', shell=True) 51 | fprint('') 52 | 53 | fprint('Execute Static Test: mypy') 54 | subprocess.run('mypy vmbpy', shell=True, check=True) 55 | fprint('') 56 | 57 | 58 | def unit_test(testsuite, testcamera, blacklist): 59 | blacklist = " ".join(blacklist) 60 | 61 | fprint('Execute Unit tests and measure coverage:') 62 | if testsuite == 'basic': 63 | cmd = 'coverage run Tests/runner.py -s basic --console {}'.format(blacklist) 64 | 65 | else: 66 | cmd = 'coverage run Tests/runner.py -s {} --console {}' 67 | cmd = cmd.format(testsuite, blacklist) 68 | if testcamera: 69 | cmd += ' -c {}'.format(testcamera) 70 | 71 | subprocess.run(cmd, shell=True, check=True) 72 | fprint('') 73 | 74 | fprint('Coverage during test execution:') 75 | subprocess.run('coverage report -m', shell=True, check=True) 76 | fprint('') 77 | 78 | coverage_file = '.coverage' 79 | if os.path.exists(coverage_file): 80 | os.remove(coverage_file) 81 | 82 | 83 | def setup_junit(report_dir): 84 | if os.path.exists(report_dir): 85 | shutil.rmtree(report_dir, ignore_errors=True) 86 | 87 | os.mkdir(report_dir) 88 | 89 | 90 | def static_test_junit(report_dir): 91 | fprint('Execute Static Test: flake8') 92 | cmd = 'flake8 vmbpy --output-file=' + report_dir + '/flake8.txt' 93 | subprocess.run(cmd, shell=True, check=True) 94 | 95 | cmd = 'flake8_junit ' + report_dir + '/flake8.txt ' + report_dir + '/flake8_junit.xml' 96 | subprocess.run(cmd, shell=True, check=True) 97 | fprint('') 98 | 99 | fprint('Execute Static Test: mypy') 100 | cmd = 'mypy vmbpy --junit-xml ' + report_dir + '/mypy_junit.xml' 101 | subprocess.run(cmd, shell=True, check=True) 102 | fprint('') 103 | 104 | 105 | def unit_test_junit(report_dir, testsuite, testcamera, blacklist): 106 | fprint('Execute Unit tests and measure coverage:') 107 | 108 | blacklist = " ".join(blacklist) 109 | if testsuite == 'basic': 110 | cmd = 'coverage run --branch Tests/runner.py -s basic --junit_xml {} {}' 111 | cmd = cmd.format(report_dir, blacklist) 112 | 113 | else: 114 | cmd = 'coverage run --branch Tests/runner.py -s {} --junit_xml {}' 115 | cmd = cmd.format(testsuite, report_dir,) 116 | if testcamera: 117 | cmd += ' -c {}'.format(testcamera) 118 | cmd += ' {}'.format(blacklist) 119 | 120 | subprocess.run(cmd, shell=True, check=True) 121 | fprint('') 122 | 123 | fprint('Generate Coverage reports:') 124 | subprocess.run('coverage report -m', shell=True, check=True) 125 | subprocess.run('coverage xml -o ' + report_dir + '/coverage.xml', shell=True, check=True) 126 | fprint('') 127 | 128 | coverage_file = '.coverage' 129 | if os.path.exists(coverage_file): 130 | os.remove(coverage_file) 131 | 132 | 133 | def test(testsuite, testcamera, blacklist): 134 | static_test() 135 | unit_test(testsuite, testcamera, blacklist) 136 | 137 | 138 | def test_junit(report_dir, testsuite, testcamera, blacklist): 139 | setup_junit(report_dir) 140 | static_test_junit(report_dir) 141 | unit_test_junit(report_dir, testsuite, testcamera, blacklist) 142 | 143 | 144 | def main(): 145 | arg_parser = Parser() 146 | args = arg_parser.parse_args() 147 | 148 | if args.console: 149 | test(args.suite, args.camera_id, args.blacklist) 150 | 151 | elif args.junit_xml: 152 | test_junit(args.junit_xml, args.suite, args.camera_id, args.blacklist) 153 | 154 | 155 | if __name__ == '__main__': 156 | main() 157 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = vmbpy 3 | version = attr: vmbpy.__version__ 4 | author = Allied Vision Technologies 5 | description = Python bindings for VmbC 6 | long_description = file: README.md 7 | long_description_content_type = text/markdown 8 | license = BSD 2-Clause License 9 | url = https://github.com/alliedvision/vmbpy 10 | project_urls = 11 | Bug Tracker = https://github.com/alliedvision/vmbpy/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | Operating System :: OS Independent 15 | 16 | [options] 17 | packages = 18 | vmbpy 19 | vmbpy.c_binding 20 | vmbpy.util 21 | python_requires = >=3.7 22 | include_package_data = True 23 | 24 | [options.extras_require] 25 | numpy = numpy 26 | opencv = opencv-python 27 | test = 28 | unittest-xml-reporting 29 | flake8 30 | flake8-junit-report 31 | mypy 32 | coverage 33 | 34 | [flake8] 35 | max-line-length = 100 36 | 37 | [coverage:run] 38 | source = vmbpy 39 | 40 | [coverage:report] 41 | exclude_lines = 42 | coverage: skip 43 | def __repr__ 44 | def __str__ 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py 3 | isolated_build = True 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | coverage 9 | numpy 10 | commands = 11 | python -c "from Tests.runner import print_test_execution_info; print_test_execution_info()" 12 | coverage run --data-file=coverage -m pytest -v Tests --junitxml=test_results.xml --log-level=DEBUG 13 | coverage xml --data-file=coverage -o coverage.xml 14 | passenv = 15 | VIMBA_X_HOME 16 | GENICAM_GENTL64_PATH 17 | VMBPY_DEVICE_ID 18 | -------------------------------------------------------------------------------- /vmbpy/__init__.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | # Suppress 'imported but unused' - Error from static style checker. 29 | # flake8: noqa: F401 30 | 31 | __version__ = '1.1.0' 32 | 33 | __all__ = [ 34 | 'VmbSystem', 35 | 'Stream', 36 | 'Camera', 37 | 'LocalDevice', 38 | 'CameraChangeHandler', 39 | 'CameraEvent', 40 | 'AccessMode', 41 | 'PersistType', 42 | 'ModulePersistFlags', 43 | 'Interface', 44 | 'TransportLayer', 45 | 'TransportLayerType', 46 | 'InterfaceChangeHandler', 47 | 'InterfaceEvent', 48 | 'PixelFormat', 49 | 'Frame', 50 | 'FeatureTypes', 51 | 'FeatureVisibility', 52 | 'FrameHandler', 53 | 'FrameStatus', 54 | 'PayloadType', 55 | 'AllocationMode', 56 | 'Debayer', 57 | 'intersect_pixel_formats', 58 | 'MONO_PIXEL_FORMATS', 59 | 'BAYER_PIXEL_FORMATS', 60 | 'RGB_PIXEL_FORMATS', 61 | 'RGBA_PIXEL_FORMATS', 62 | 'BGR_PIXEL_FORMATS', 63 | 'BGRA_PIXEL_FORMATS', 64 | 'YUV_PIXEL_FORMATS', 65 | 'YCBCR_PIXEL_FORMATS', 66 | 'COLOR_PIXEL_FORMATS', 67 | 'OPENCV_PIXEL_FORMATS', 68 | 69 | 'VmbSystemError', 70 | 'VmbTransportLayerError', 71 | 'VmbCameraError', 72 | 'VmbInterfaceError', 73 | 'VmbFeatureError', 74 | 'VmbFrameError', 75 | 'VmbTimeout', 76 | 'VmbChunkError', 77 | 78 | 'IntFeature', 79 | 'FloatFeature', 80 | 'StringFeature', 81 | 'BoolFeature', 82 | 'EnumEntry', 83 | 'EnumFeature', 84 | 'CommandFeature', 85 | 'RawFeature', 86 | 87 | 'FeatureContainer', 88 | 'PersistableFeatureContainer', 89 | 90 | 'LogLevel', 91 | 'LogConfig', 92 | 'Log', 93 | 'LOG_CONFIG_TRACE_CONSOLE_ONLY', 94 | 'LOG_CONFIG_TRACE_FILE_ONLY', 95 | 'LOG_CONFIG_TRACE', 96 | 'LOG_CONFIG_DEBUG_CONSOLE_ONLY', 97 | 'LOG_CONFIG_DEBUG_FILE_ONLY', 98 | 'LOG_CONFIG_DEBUG', 99 | 'LOG_CONFIG_INFO_CONSOLE_ONLY', 100 | 'LOG_CONFIG_INFO_FILE_ONLY', 101 | 'LOG_CONFIG_INFO', 102 | 'LOG_CONFIG_WARNING_CONSOLE_ONLY', 103 | 'LOG_CONFIG_WARNING_FILE_ONLY', 104 | 'LOG_CONFIG_WARNING', 105 | 'LOG_CONFIG_ERROR_CONSOLE_ONLY', 106 | 'LOG_CONFIG_ERROR_FILE_ONLY', 107 | 'LOG_CONFIG_ERROR', 108 | 'LOG_CONFIG_CRITICAL_CONSOLE_ONLY', 109 | 'LOG_CONFIG_CRITICAL_FILE_ONLY', 110 | 'LOG_CONFIG_CRITICAL', 111 | 112 | 'TraceEnable', 113 | 'ScopedLogEnable', 114 | 'RuntimeTypeCheckEnable', 115 | 'VmbIntEnum', 116 | 'VmbFlagEnum' 117 | ] 118 | 119 | from .camera import AccessMode, Camera, CameraChangeHandler, CameraEvent 120 | from .error import (VmbCameraError, VmbChunkError, VmbFeatureError, VmbFrameError, 121 | VmbInterfaceError, VmbSystemError, VmbTimeout, VmbTransportLayerError) 122 | from .feature import (BoolFeature, CommandFeature, EnumEntry, EnumFeature, FeatureTypes, 123 | FeatureVisibility, FloatFeature, IntFeature, RawFeature, StringFeature) 124 | from .featurecontainer import (FeatureContainer, ModulePersistFlags, PersistableFeatureContainer, 125 | PersistType) 126 | from .frame import (BAYER_PIXEL_FORMATS, BGR_PIXEL_FORMATS, BGRA_PIXEL_FORMATS, COLOR_PIXEL_FORMATS, 127 | MONO_PIXEL_FORMATS, OPENCV_PIXEL_FORMATS, RGB_PIXEL_FORMATS, RGBA_PIXEL_FORMATS, 128 | YCBCR_PIXEL_FORMATS, YUV_PIXEL_FORMATS, AllocationMode, Debayer, Frame, 129 | FrameStatus, PayloadType, PixelFormat, intersect_pixel_formats) 130 | from .interface import Interface, InterfaceChangeHandler, InterfaceEvent 131 | from .localdevice import LocalDevice 132 | from .stream import FrameHandler, Stream 133 | from .transportlayer import TransportLayer, TransportLayerType 134 | from .util import (LOG_CONFIG_CRITICAL, LOG_CONFIG_CRITICAL_CONSOLE_ONLY, 135 | LOG_CONFIG_CRITICAL_FILE_ONLY, LOG_CONFIG_DEBUG, LOG_CONFIG_DEBUG_CONSOLE_ONLY, 136 | LOG_CONFIG_DEBUG_FILE_ONLY, LOG_CONFIG_ERROR, LOG_CONFIG_ERROR_CONSOLE_ONLY, 137 | LOG_CONFIG_ERROR_FILE_ONLY, LOG_CONFIG_INFO, LOG_CONFIG_INFO_CONSOLE_ONLY, 138 | LOG_CONFIG_INFO_FILE_ONLY, LOG_CONFIG_TRACE, LOG_CONFIG_TRACE_CONSOLE_ONLY, 139 | LOG_CONFIG_TRACE_FILE_ONLY, LOG_CONFIG_WARNING, LOG_CONFIG_WARNING_CONSOLE_ONLY, 140 | LOG_CONFIG_WARNING_FILE_ONLY, Log, LogConfig, LogLevel, RuntimeTypeCheckEnable, 141 | ScopedLogEnable, TraceEnable, VmbIntEnum, VmbFlagEnum) 142 | from .vmbsystem import VmbSystem 143 | -------------------------------------------------------------------------------- /vmbpy/c_binding/__init__.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | ------------------------------------------------------------------------- 28 | 29 | NOTE: Vmb naming convention. 30 | VmbPy is based on VmbC, this submodule contains all wrapped types and functions provided by VmbC to 31 | make them usable from Python. By convention, all VmbC Types and Functions are prefixed with 'Vmb', 32 | this convention is kept for all python types interfacing with the C - Layer. VmbC developers should 33 | be able to understand the interface to VmbC and keeping the name convention helps a lot in that 34 | regard. 35 | 36 | However prefixing everything with 'Vmb' is not required in VmbPy, therefore most Types of the public 37 | API have no prefix. 38 | """ 39 | 40 | # Suppress 'imported but unused' - Error from static style checker. 41 | # flake8: noqa: F401 42 | 43 | __all__ = [ 44 | # Exports from vmb_common 45 | 'VmbInt8', 46 | 'VmbUint8', 47 | 'VmbInt16', 48 | 'VmbUint16', 49 | 'VmbInt32', 50 | 'VmbUint32', 51 | 'VmbInt64', 52 | 'VmbUint64', 53 | 'VmbHandle', 54 | 'VmbBool', 55 | 'VmbUchar', 56 | 'VmbDouble', 57 | 'VmbError', 58 | 'VmbCError', 59 | 'VmbPixelFormat', 60 | 'decode_cstr', 61 | 'decode_flags', 62 | 63 | # Exports from vmb_c 64 | 'VmbTransportLayer', 65 | 'VmbAccessMode', 66 | 'VmbFeatureData', 67 | 'VmbFeaturePersist', 68 | 'VmbModulePersistFlags', 69 | 'VmbFeatureVisibility', 70 | 'VmbFeatureFlags', 71 | 'VmbFrameStatus', 72 | 'VmbPayloadType', 73 | 'VmbFrameFlags', 74 | 'VmbVersionInfo', 75 | 'VmbTransportLayerInfo', 76 | 'VmbInterfaceInfo', 77 | 'VmbCameraInfo', 78 | 'VmbFeatureInfo', 79 | 'VmbFeatureEnumEntry', 80 | 'VmbFrame', 81 | 'VmbFeaturePersistSettings', 82 | 'G_VMB_C_HANDLE', 83 | 'VMB_C_VERSION', 84 | 'EXPECTED_VMB_C_VERSION', 85 | 'call_vmb_c', 86 | 87 | # Exports from vmb_image_transform 88 | 'VmbImage', 89 | 'VmbImageInfo', 90 | 'VmbDebayerMode', 91 | 'VmbTransformInfo', 92 | 'VMB_IMAGE_TRANSFORM_VERSION', 93 | 'EXPECTED_VMB_IMAGE_TRANSFORM_VERSION', 94 | 'call_vmb_image_transform', 95 | 'PIXEL_FORMAT_TO_LAYOUT', 96 | 'LAYOUT_TO_PIXEL_FORMAT', 97 | 'PIXEL_FORMAT_CONVERTIBILITY_MAP', 98 | 99 | # Exports from wrapped_types 100 | 'AccessMode', 101 | 'Debayer', 102 | 'FeatureFlags', 103 | 'FeatureVisibility', 104 | 'FrameStatus', 105 | 'PayloadType', 106 | 'PersistType', 107 | 'ModulePersistFlags', 108 | 'PixelFormat', 109 | 'TransportLayerType', 110 | 111 | # Exports from ctypes 112 | 'byref', 113 | 'sizeof', 114 | 'create_string_buffer' 115 | ] 116 | 117 | from ctypes import byref, create_string_buffer, sizeof 118 | 119 | from .vmb_c import (EXPECTED_VMB_C_VERSION, G_VMB_C_HANDLE, VMB_C_VERSION, VmbAccessMode, 120 | VmbCameraInfo, VmbFeatureData, VmbFeatureEnumEntry, VmbFeatureFlags, 121 | VmbFeatureInfo, VmbFeaturePersist, VmbFeaturePersistSettings, 122 | VmbFeatureVisibility, VmbFrame, VmbFrameFlags, VmbFrameStatus, VmbPayloadType, 123 | VmbInterfaceInfo, VmbModulePersistFlags, VmbTransportLayer, VmbTransportLayerInfo, 124 | VmbVersionInfo, call_vmb_c) 125 | from .vmb_common import (VmbBool, VmbCError, VmbDouble, VmbError, VmbHandle, VmbInt8, VmbInt16, 126 | VmbInt32, VmbInt64, VmbPixelFormat, VmbUchar, VmbUint8, VmbUint16, 127 | VmbUint32, VmbUint64, _as_vmb_file_path, _select_vimbax_home, decode_cstr, 128 | decode_flags) 129 | from .vmb_image_transform import (EXPECTED_VMB_IMAGE_TRANSFORM_VERSION, LAYOUT_TO_PIXEL_FORMAT, 130 | PIXEL_FORMAT_CONVERTIBILITY_MAP, PIXEL_FORMAT_TO_LAYOUT, 131 | VMB_IMAGE_TRANSFORM_VERSION, VmbDebayerMode, VmbImage, 132 | VmbImageInfo, VmbTransformInfo, call_vmb_image_transform) 133 | from .wrapped_types import (AccessMode, Debayer, FeatureFlags, FeatureVisibility, FrameStatus, 134 | PayloadType, ModulePersistFlags, PersistType, PixelFormat, TransportLayerType) 135 | -------------------------------------------------------------------------------- /vmbpy/error.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | from .util import Log 28 | 29 | __all__ = [ 30 | 'VmbSystemError', 31 | 'VmbTransportLayerError', 32 | 'VmbCameraError', 33 | 'VmbInterfaceError', 34 | 'VmbFeatureError', 35 | 'VmbFrameError', 36 | 'VmbTimeout' 37 | ] 38 | 39 | 40 | class _LoggedError(Exception): 41 | def __init__(self, msg: str): 42 | super().__init__(msg) 43 | Log.get_instance().error(msg) 44 | 45 | 46 | class VmbSystemError(_LoggedError): 47 | """Errors related to the underlying Vimba X System 48 | 49 | Error type to indicate system-wide errors like: 50 | 51 | - Incomplete Vimba X installation 52 | - Incompatible version of the underlying C-Layer 53 | - An unsupported OS 54 | """ 55 | pass 56 | 57 | 58 | class VmbTransportLayerError(_LoggedError): 59 | """Errors related to Transport Layers 60 | 61 | Error type to indicate Transport Layer related errors like: 62 | 63 | - Lookup of a non-existing Transport Layer 64 | """ 65 | pass 66 | 67 | 68 | class VmbCameraError(_LoggedError): 69 | """Errors related to cameras 70 | 71 | Error Type to indicate camera-related errors like: 72 | 73 | - Access of a disconnected Camera object 74 | - Lookup of non-existing cameras 75 | """ 76 | pass 77 | 78 | 79 | class VmbInterfaceError(_LoggedError): 80 | """Errors related to Interfaces 81 | 82 | Error Type to indicated interface-related errors like: 83 | 84 | - Access on a disconnected Interface object 85 | - Lookup of a non-existing Interface 86 | """ 87 | pass 88 | 89 | 90 | class VmbFeatureError(_LoggedError): 91 | """Error related to Feature access 92 | 93 | Error type to indicate invalid Feature access like: 94 | 95 | - Invalid access mode on Feature access. 96 | - Out of range values upon setting a value. 97 | - Failed lookup of features. 98 | """ 99 | pass 100 | 101 | 102 | class VmbFrameError(_LoggedError): 103 | """Error related to Frame data""" 104 | pass 105 | 106 | 107 | class VmbTimeout(_LoggedError): 108 | """Indicates that an operation timed out""" 109 | pass 110 | 111 | 112 | class VmbChunkError(_LoggedError): 113 | """Errors related to chunk data and chunk access""" 114 | pass 115 | -------------------------------------------------------------------------------- /vmbpy/interface.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | from __future__ import annotations 28 | 29 | from typing import TYPE_CHECKING, Callable, Dict, Tuple 30 | 31 | from .c_binding import VmbHandle, VmbInterfaceInfo, decode_cstr 32 | from .featurecontainer import PersistableFeatureContainer 33 | from .shared import read_memory, write_memory 34 | from .transportlayer import TransportLayerType 35 | from .util import (EnterContextOnCall, LeaveContextOnCall, RaiseIfOutsideContext, 36 | RuntimeTypeCheckEnable, TraceEnable, VmbIntEnum) 37 | 38 | if TYPE_CHECKING: 39 | from .camera import CamerasTuple 40 | from .transportlayer import TransportLayer 41 | 42 | __all__ = [ 43 | 'Interface', 44 | 'InterfaceEvent', 45 | 'InterfaceChangeHandler', 46 | 'InterfacesTuple', 47 | 'InterfacesDict' 48 | ] 49 | 50 | 51 | # Forward declarations 52 | InterfaceChangeHandler = Callable[['Interface', 'InterfaceEvent'], None] 53 | InterfacesTuple = Tuple['Interface', ...] 54 | InterfacesDict = Dict[VmbHandle, 'Interface'] 55 | 56 | 57 | class InterfaceEvent(VmbIntEnum): 58 | """Enum specifying an Interface Event""" 59 | Missing = 0 #: A known interface disappeared from the bus 60 | Detected = 1 #: A new interface was discovered 61 | Reachable = 2 #: A known interface can be accessed 62 | Unreachable = 3 #: A known interface cannot be accessed anymore 63 | 64 | 65 | class Interface(PersistableFeatureContainer): 66 | """This class allows access to an interface such as USB detected by VmbC.""" 67 | 68 | @TraceEnable() 69 | def __init__(self, info: VmbInterfaceInfo, transport_layer: TransportLayer): 70 | """Do not call directly. Access Interfaces via ``vmbpy.VmbSystem`` instead.""" 71 | super().__init__() 72 | self.__transport_layer = transport_layer 73 | self.__info: VmbInterfaceInfo = info 74 | self._handle: VmbHandle = self.__info.interfaceHandle 75 | self._open() 76 | 77 | @TraceEnable() 78 | @EnterContextOnCall() 79 | def _open(self): 80 | self._attach_feature_accessors() 81 | 82 | @TraceEnable() 83 | @LeaveContextOnCall() 84 | def _close(self): 85 | self._remove_feature_accessors() 86 | 87 | def __str__(self): 88 | return 'Interface(id={})'.format(self.get_id()) 89 | 90 | def __repr__(self): 91 | rep = 'Interface' 92 | rep += '(_handle=' + repr(self._handle) 93 | rep += ',__info=' + repr(self.__info) 94 | rep += ')' 95 | return rep 96 | 97 | def get_id(self) -> str: 98 | """Get Interface Id such as 'VimbaUSBInterface_0x0'.""" 99 | return decode_cstr(self.__info.interfaceIdString) 100 | 101 | def get_type(self) -> TransportLayerType: 102 | """Get Interface Type such as ``TransportLayerType.GEV``. 103 | 104 | Note: 105 | This uses the ``TransportLayerType`` enum to report the connection type of the Interface 106 | as there is no dedicated interface type enum. The ``TransportLayerType`` covers all 107 | interface types. 108 | """ 109 | return TransportLayerType(self.__info.interfaceType) 110 | 111 | def get_name(self) -> str: 112 | """Get Interface Name such as 'VimbaX USB Interface'.""" 113 | return decode_cstr(self.__info.interfaceName) 114 | 115 | @TraceEnable() 116 | @RuntimeTypeCheckEnable() 117 | def read_memory(self, addr: int, max_bytes: int) -> bytes: # coverage: skip 118 | """Read a byte sequence from a given memory address. 119 | 120 | Arguments: 121 | addr: 122 | Starting address to read from. 123 | max_bytes: 124 | Maximum number of bytes to read from addr. 125 | 126 | Returns: 127 | Read memory contents as bytes. 128 | 129 | Raises: 130 | TypeError: 131 | If parameters do not match their type hint. 132 | ValueError: 133 | If ``addr`` is negative. 134 | ValueError: 135 | If ``max_bytes`` is negative. 136 | ValueError: 137 | If the memory access was invalid. 138 | """ 139 | # Note: Coverage is skipped. Function is untestable in a generic way. 140 | return read_memory(self._handle, addr, max_bytes) 141 | 142 | @TraceEnable() 143 | @RuntimeTypeCheckEnable() 144 | def write_memory(self, addr: int, data: bytes): # coverage: skip 145 | """Write a byte sequence to a given memory address. 146 | 147 | Arguments: 148 | addr: 149 | Address to write the content of 'data' to. 150 | data: 151 | Byte sequence to write at address 'addr'. 152 | 153 | Raises: 154 | TypeError: 155 | If parameters do not match their type hint. 156 | ValueError: 157 | If ``addr`` is negative. 158 | """ 159 | # Note: Coverage is skipped. Function is untestable in a generic way. 160 | return write_memory(self._handle, addr, data) 161 | 162 | def get_transport_layer(self) -> TransportLayer: 163 | """Get the Transport Layer associated with this instance of Interface""" 164 | return self.__transport_layer 165 | 166 | def get_cameras(self) -> CamerasTuple: 167 | """Get access to cameras associated with the Interface instance 168 | 169 | Returns: 170 | A tuple of all cameras associated with this Interface 171 | 172 | Raises: 173 | RuntimeError: 174 | If called outside of VmbSystem ``with`` context. 175 | """ 176 | return self._get_cameras() 177 | 178 | def _get_cameras(self): 179 | # This method relies on functionality of `VmbSystem` and is overwritten from there. This is 180 | # done to avoid importing `VmbSystem` here which would lead to a circular dependency. 181 | raise NotImplementedError 182 | 183 | def _get_handle(self) -> VmbHandle: 184 | """Internal helper function to get handle of interface""" 185 | return self._handle 186 | 187 | _msg = 'Called \'{}()\' outside of VmbSystems \'with\' context.' 188 | get_all_features = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_all_features) # noqa: E501 189 | get_features_selected_by = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_features_selected_by) # noqa: E501 190 | get_features_by_type = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_features_by_type) # noqa: E501 191 | get_features_by_category = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_features_by_category) # noqa: E501 192 | get_feature_by_name = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_feature_by_name) # noqa: E501 193 | load_settings = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.load_settings) 194 | save_settings = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.save_settings) 195 | -------------------------------------------------------------------------------- /vmbpy/localdevice.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | from .c_binding import VmbHandle 29 | from .featurecontainer import PersistableFeatureContainer 30 | from .util import EnterContextOnCall, LeaveContextOnCall, RaiseIfOutsideContext 31 | 32 | __all__ = [ 33 | 'LocalDevice' 34 | ] 35 | 36 | 37 | class LocalDevice(PersistableFeatureContainer): 38 | """This class provides access to the Local Device of a Camera. 39 | """ 40 | def __init__(self, handle: VmbHandle) -> None: 41 | super().__init__() 42 | self._handle: VmbHandle = handle 43 | self._open() 44 | 45 | @EnterContextOnCall() 46 | def _open(self): 47 | self._attach_feature_accessors() 48 | 49 | @LeaveContextOnCall() 50 | def _close(self): 51 | self._remove_feature_accessors() 52 | 53 | __msg = 'Called \'{}()\' outside of Cameras \'with\' context.' 54 | get_all_features = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.get_all_features) # noqa: E501 55 | get_features_selected_by = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.get_features_selected_by) # noqa: E501 56 | get_features_by_type = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.get_features_by_type) # noqa: E501 57 | get_features_by_category = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.get_features_by_category) # noqa: E501 58 | get_feature_by_name = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.get_feature_by_name) # noqa: E501 59 | load_settings = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.load_settings) 60 | save_settings = RaiseIfOutsideContext(msg=__msg)(PersistableFeatureContainer.save_settings) 61 | -------------------------------------------------------------------------------- /vmbpy/transportlayer.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | from __future__ import annotations 28 | 29 | from typing import TYPE_CHECKING, Dict, Tuple 30 | 31 | from .c_binding import TransportLayerType, VmbHandle, VmbTransportLayerInfo, decode_cstr 32 | from .featurecontainer import PersistableFeatureContainer 33 | from .util import EnterContextOnCall, LeaveContextOnCall, RaiseIfOutsideContext, TraceEnable 34 | 35 | if TYPE_CHECKING: 36 | from .camera import CamerasTuple 37 | from .interface import InterfacesTuple 38 | 39 | 40 | __all__ = [ 41 | 'TransportLayer', 42 | 'TransportLayerType', 43 | 'TransportLayersTuple', 44 | 'TransportLayersDict' 45 | ] 46 | 47 | # Forward declarations 48 | TransportLayersTuple = Tuple['TransportLayer', ...] 49 | TransportLayersDict = Dict[VmbHandle, 'TransportLayer'] 50 | 51 | 52 | class TransportLayer(PersistableFeatureContainer): 53 | """This class allows access to a Transport Layer.""" 54 | 55 | @TraceEnable() 56 | def __init__(self, info: VmbTransportLayerInfo): 57 | """Do not call directly. Access Transport Layers via ``vmbpy.VmbSystem`` instead.""" 58 | super().__init__() 59 | self.__info: VmbTransportLayerInfo = info 60 | self._handle: VmbHandle = self.__info.transportLayerHandle 61 | self._open() 62 | 63 | def __str__(self): 64 | return 'TransportLayer(id={})'.format(self.get_id()) 65 | 66 | def __repr__(self) -> str: 67 | rep = 'TransportLayer' 68 | rep += '(_handle=' + repr(self._handle) 69 | rep += ',__info=' + repr(self.__info) 70 | rep += ')' 71 | return rep 72 | 73 | @TraceEnable() 74 | @EnterContextOnCall() 75 | def _open(self): 76 | self._attach_feature_accessors() 77 | 78 | @TraceEnable() 79 | @LeaveContextOnCall() 80 | def _close(self): 81 | self._remove_feature_accessors() 82 | 83 | def get_interfaces(self) -> InterfacesTuple: 84 | """Get all interfaces associated with the Transport Layer instance. 85 | 86 | Returns: 87 | A tuple of all interfaces associated with this Transport Layer. 88 | 89 | Raises: 90 | RuntimeError: 91 | If called outside of VmbSystem ``with`` context. 92 | """ 93 | return self._get_interfaces() 94 | 95 | def _get_interfaces(self): 96 | # This method is implemented using functionality of `VmbSystem`. This is just a placeholder 97 | # that is overwritten from `VmbSystem`. This is done to avoid importing `VmbSystem` here 98 | # which would lead to a circular dependency. 99 | raise NotImplementedError 100 | 101 | def get_cameras(self) -> CamerasTuple: 102 | """Get access to cameras associated with the Transport Layer instance. 103 | 104 | Returns: 105 | A tuple of all cameras associated with this Transport Layer. 106 | 107 | Raises: 108 | RuntimeError: 109 | If called outside of VmbSystem ``with`` context. 110 | """ 111 | return self._get_cameras() 112 | 113 | def _get_cameras(self): 114 | # This method relies on functionality of `VmbSystem` and is overwritten from there. This is 115 | # done to avoid importing `VmbSystem` here which would lead to a circular dependency. 116 | raise NotImplementedError 117 | 118 | def get_id(self) -> str: 119 | """Get Transport Layer Id such as 'VimbaGigETL'""" 120 | return decode_cstr(self.__info.transportLayerIdString) 121 | 122 | def get_name(self) -> str: 123 | """Get Transport Layer Display Name such as 'AVT GigE Transport Layer'""" 124 | return decode_cstr(self.__info.transportLayerName) 125 | 126 | def get_model_name(self) -> str: 127 | """Get Transport Layer Model Name such as 'GigETL'""" 128 | return decode_cstr(self.__info.transportLayerModelName) 129 | 130 | def get_vendor(self) -> str: 131 | """Get Transport Layer Vendor such as 'Allied Vision Technologies'""" 132 | return decode_cstr(self.__info.transportLayerVendor) 133 | 134 | def get_version(self) -> str: 135 | """Get Transport Layer Version""" 136 | return decode_cstr(self.__info.transportLayerVersion) 137 | 138 | def get_path(self) -> str: 139 | """Get path to Transport Layer file""" 140 | return decode_cstr(self.__info.transportLayerPath) 141 | 142 | def get_type(self) -> TransportLayerType: 143 | """Get Transport Layer Type such as ``TransportLayerType.GEV``""" 144 | return TransportLayerType(self.__info.transportLayerType) 145 | 146 | def _get_handle(self) -> VmbHandle: 147 | """Internal helper function to get handle of Transport Layer""" 148 | return self._handle 149 | 150 | _msg = 'Called \'{}()\' outside of VmbSystems \'with\' context.' 151 | get_all_features = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_all_features) # noqa: E501 152 | get_features_selected_by = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_features_selected_by) # noqa: E501 153 | get_features_by_type = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_features_by_type) # noqa: E501 154 | get_features_by_category = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_features_by_category) # noqa: E501 155 | get_feature_by_name = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.get_feature_by_name) # noqa: E501 156 | load_settings = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.load_settings) 157 | save_settings = RaiseIfOutsideContext(msg=_msg)(PersistableFeatureContainer.save_settings) 158 | -------------------------------------------------------------------------------- /vmbpy/util/__init__.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | # Suppress 'imported but unused' - Error from static style checker. 29 | # flake8: noqa: F401 30 | 31 | __all__ = [ 32 | 'LogLevel', 33 | 'LogConfig', 34 | 'Log', 35 | 'LOG_CONFIG_TRACE_CONSOLE_ONLY', 36 | 'LOG_CONFIG_TRACE_FILE_ONLY', 37 | 'LOG_CONFIG_TRACE', 38 | 'LOG_CONFIG_DEBUG_CONSOLE_ONLY', 39 | 'LOG_CONFIG_DEBUG_FILE_ONLY', 40 | 'LOG_CONFIG_DEBUG', 41 | 'LOG_CONFIG_INFO_CONSOLE_ONLY', 42 | 'LOG_CONFIG_INFO_FILE_ONLY', 43 | 'LOG_CONFIG_INFO', 44 | 'LOG_CONFIG_WARNING_CONSOLE_ONLY', 45 | 'LOG_CONFIG_WARNING_FILE_ONLY', 46 | 'LOG_CONFIG_WARNING', 47 | 'LOG_CONFIG_ERROR_CONSOLE_ONLY', 48 | 'LOG_CONFIG_ERROR_FILE_ONLY', 49 | 'LOG_CONFIG_ERROR', 50 | 'LOG_CONFIG_CRITICAL_CONSOLE_ONLY', 51 | 'LOG_CONFIG_CRITICAL_FILE_ONLY', 52 | 'LOG_CONFIG_CRITICAL', 53 | 54 | # Decorators 55 | 'TraceEnable', 56 | 'ScopedLogEnable', 57 | 'RuntimeTypeCheckEnable', 58 | 'EnterContextOnCall', 59 | 'LeaveContextOnCall', 60 | 'RaiseIfInsideContext', 61 | 'RaiseIfOutsideContext', 62 | 63 | # Enums 64 | 'VmbIntEnum', 65 | 'VmbFlagEnum' 66 | ] 67 | 68 | from .context_decorator import (EnterContextOnCall, LeaveContextOnCall, RaiseIfInsideContext, 69 | RaiseIfOutsideContext) 70 | from .vmb_enum import VmbIntEnum, VmbFlagEnum 71 | from .log import (LOG_CONFIG_CRITICAL, LOG_CONFIG_CRITICAL_CONSOLE_ONLY, 72 | LOG_CONFIG_CRITICAL_FILE_ONLY, LOG_CONFIG_DEBUG, LOG_CONFIG_DEBUG_CONSOLE_ONLY, 73 | LOG_CONFIG_DEBUG_FILE_ONLY, LOG_CONFIG_ERROR, LOG_CONFIG_ERROR_CONSOLE_ONLY, 74 | LOG_CONFIG_ERROR_FILE_ONLY, LOG_CONFIG_INFO, LOG_CONFIG_INFO_CONSOLE_ONLY, 75 | LOG_CONFIG_INFO_FILE_ONLY, LOG_CONFIG_TRACE, LOG_CONFIG_TRACE_CONSOLE_ONLY, 76 | LOG_CONFIG_TRACE_FILE_ONLY, LOG_CONFIG_WARNING, LOG_CONFIG_WARNING_CONSOLE_ONLY, 77 | LOG_CONFIG_WARNING_FILE_ONLY, Log, LogConfig, LogLevel) 78 | from .runtime_type_check import RuntimeTypeCheckEnable 79 | from .scoped_log import ScopedLogEnable 80 | from .tracer import TraceEnable 81 | -------------------------------------------------------------------------------- /vmbpy/util/context_decorator.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import functools 28 | from typing import Any, Callable, TypeVar 29 | 30 | __all__ = [ 31 | 'EnterContextOnCall', 32 | 'LeaveContextOnCall', 33 | 'RaiseIfInsideContext', 34 | 'RaiseIfOutsideContext' 35 | ] 36 | 37 | # TODO: To improve type checking further consider usage of `ParamSpec`. Introduced to standard 38 | # library with Python 3.10 and available as backport in `typing_extensions` package for Python >=3.8 39 | T = TypeVar('T') 40 | 41 | 42 | class EnterContextOnCall: 43 | """Decorator setting/injecting flag used for checking the context.""" 44 | 45 | def __call__(self, func: Callable[..., T]) -> Callable[..., T]: 46 | @functools.wraps(func) 47 | def wrapper(*args: Any, **kwargs: Any) -> T: 48 | args[0]._context_entered = True 49 | try: 50 | return func(*args, **kwargs) 51 | except Exception: 52 | # If an error occurs during the function call, we do not consider the context to be 53 | # entered 54 | args[0]._context_entered = False 55 | raise 56 | 57 | return wrapper 58 | 59 | 60 | class LeaveContextOnCall: 61 | """Decorator clearing/injecting flag used for checking the context.""" 62 | 63 | def __call__(self, func: Callable[..., T]) -> Callable[..., T]: 64 | @functools.wraps(func) 65 | def wrapper(*args: Any, **kwargs: Any) -> T: 66 | result = func(*args, **kwargs) 67 | args[0]._context_entered = False 68 | return result 69 | return wrapper 70 | 71 | 72 | class RaiseIfInsideContext: 73 | """Raising RuntimeError is decorated Method is called inside with-statement. 74 | 75 | Note: 76 | This Decorator shall work only on Object implementing a Context Manger. For this to work 77 | object must offer a boolean attribute called ``_context_entered`` 78 | """ 79 | 80 | def __init__(self, msg='Called \'{}()\' inside of \'with\' context.'): 81 | self.msg = msg 82 | 83 | def __call__(self, func: Callable[..., T]) -> Callable[..., T]: 84 | @functools.wraps(func) 85 | def wrapper(*args: Any, **kwargs: Any) -> T: 86 | if args[0]._context_entered: 87 | msg = self.msg.format('{}'.format(func.__qualname__)) 88 | raise RuntimeError(msg) 89 | 90 | return func(*args, **kwargs) 91 | return wrapper 92 | 93 | 94 | class RaiseIfOutsideContext: 95 | """Raising RuntimeError is decorated Method is called outside with-statement. 96 | 97 | Note: 98 | This Decorator shall work only on Object implementing a Context Manger. For this to work 99 | object must offer a boolean attribute called ``_context_entered`` 100 | """ 101 | 102 | def __init__(self, msg='Called \'{}()\' outside of \'with\' context.'): 103 | self.msg = msg 104 | 105 | def __call__(self, func: Callable[..., T]) -> Callable[..., T]: 106 | @functools.wraps(func) 107 | def wrapper(*args: Any, **kwargs: Any) -> T: 108 | if not args[0]._context_entered: 109 | msg = self.msg.format('{}'.format(func.__qualname__)) 110 | raise RuntimeError(msg) 111 | 112 | return func(*args, **kwargs) 113 | return wrapper 114 | -------------------------------------------------------------------------------- /vmbpy/util/runtime_type_check.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import collections.abc 28 | from functools import wraps 29 | from inspect import isfunction, ismethod, signature 30 | from typing import Union, get_type_hints, Callable, TypeVar, Any 31 | 32 | from .log import Log 33 | 34 | __all__ = [ 35 | 'RuntimeTypeCheckEnable' 36 | ] 37 | 38 | # TODO: To improve type checking further consider usage of `ParamSpec`. Introduced to standard 39 | # library with Python 3.10 and available as backport in `typing_extensions` package for Python >=3.8 40 | T = TypeVar('T') 41 | 42 | 43 | class RuntimeTypeCheckEnable: 44 | """Decorator adding runtime type checking to the wrapped callable. 45 | 46 | Each time the callable is executed, all arguments are checked if they match with the given type 47 | hints. If all checks are passed, the wrapped function is executed, if the given arguments to not 48 | match a TypeError is raised. 49 | 50 | Note: 51 | This decorator is no replacement for a feature complete TypeChecker. It supports only a 52 | subset of all types expressible by type hints. 53 | """ 54 | _log = Log.get_instance() 55 | 56 | def __call__(self, func: Callable[..., T]) -> Callable[..., T]: 57 | @wraps(func) 58 | def wrapper(*args: Any, **kwargs: Any) -> T: 59 | full_args, hints = self.__dismantle_sig(func, *args, **kwargs) 60 | 61 | for arg_name in hints: 62 | self.__verify_arg(func, hints[arg_name], (arg_name, full_args[arg_name])) 63 | 64 | return func(*args, **kwargs) 65 | 66 | return wrapper 67 | 68 | def __dismantle_sig(self, func, *args, **kwargs): 69 | # Get merge args, kwargs and defaults to complete argument list. 70 | full_args = signature(func).bind(*args, **kwargs) 71 | full_args.apply_defaults() 72 | 73 | # Get available type hints, remove return value. 74 | while hasattr(func, '__wrapped__'): 75 | # Workaround for Python bug with type hints for wrapped functions 76 | # https://bugs.python.org/issue37838 77 | func = func.__wrapped__ 78 | hints = get_type_hints(func) 79 | hints.pop('return', None) 80 | 81 | return (full_args.arguments, hints) 82 | 83 | def __verify_arg(self, func, type_hint, arg_spec): 84 | arg_name, arg = arg_spec 85 | 86 | if (self.__matches(type_hint, arg)): 87 | return 88 | 89 | msg = '\'{}\' called with unexpected argument type. Argument\'{}\'. Expected type: {}.' 90 | msg = msg.format(func.__qualname__, arg_name, type_hint) 91 | 92 | RuntimeTypeCheckEnable._log.error(msg) 93 | raise TypeError(msg) 94 | 95 | def __matches(self, type_hint, arg) -> bool: 96 | if self.__matches_base_types(type_hint, arg): 97 | return True 98 | 99 | elif self.__matches_type_types(type_hint, arg): 100 | return True 101 | 102 | elif self.__matches_union_types(type_hint, arg): 103 | return True 104 | 105 | elif self.__matches_tuple_types(type_hint, arg): 106 | return True 107 | 108 | elif self.__matches_dict_types(type_hint, arg): 109 | return True 110 | 111 | else: 112 | return self.__matches_callable(type_hint, arg) 113 | 114 | def __matches_base_types(self, type_hint, arg) -> bool: 115 | return type_hint == type(arg) 116 | 117 | def __matches_type_types(self, type_hint, arg) -> bool: 118 | try: 119 | if not type_hint.__origin__ == type: 120 | return False 121 | 122 | hint_args = type_hint.__args__ 123 | 124 | except AttributeError: 125 | return False 126 | 127 | return arg in hint_args 128 | 129 | def __matches_union_types(self, type_hint, arg) -> bool: 130 | try: 131 | if not type_hint.__origin__ == Union: 132 | return False 133 | 134 | except AttributeError: 135 | return False 136 | 137 | # If Matches if true for an Union hint: 138 | for hint in type_hint.__args__: 139 | if self.__matches(hint, arg): 140 | return True 141 | 142 | return False 143 | 144 | def __matches_tuple_types(self, type_hint, arg) -> bool: 145 | try: 146 | if not (type_hint.__origin__ == tuple and isinstance(arg, tuple)): 147 | return False 148 | 149 | except AttributeError: 150 | return False 151 | 152 | if arg == (): 153 | return True 154 | 155 | if Ellipsis in type_hint.__args__: 156 | fn = self.__matches_var_length_tuple 157 | 158 | else: 159 | fn = self.__matches_fixed_size_tuple 160 | 161 | return fn(type_hint, arg) 162 | 163 | def __matches_fixed_size_tuple(self, type_hint, arg) -> bool: 164 | # To pass, the entire tuple must match in length and all types 165 | expand_hint = type_hint.__args__ 166 | 167 | if len(expand_hint) != len(arg): 168 | return False 169 | 170 | for hint, value in zip(expand_hint, arg): 171 | if not self.__matches(hint, value): 172 | return False 173 | 174 | return True 175 | 176 | def __matches_var_length_tuple(self, type_hint, arg) -> bool: 177 | # To pass a tuple can be empty or all contents must match the given type. 178 | hint, _ = type_hint.__args__ 179 | 180 | for value in arg: 181 | if not self.__matches(hint, value): 182 | return False 183 | 184 | return True 185 | 186 | def __matches_dict_types(self, type_hint, arg) -> bool: 187 | # To pass the hint must be a Dictionary and arg must match the given types. 188 | try: 189 | if not (type_hint.__origin__ == dict and isinstance(arg, dict)): 190 | return False 191 | 192 | except AttributeError: 193 | return False 194 | 195 | key_type, val_type = type_hint.__args__ 196 | 197 | for k, v in arg.items(): 198 | if not isinstance(k, key_type) or not isinstance(v, val_type): 199 | return False 200 | 201 | return True 202 | 203 | def __matches_callable(self, type_hint, arg) -> bool: 204 | # Return if the given hint is no callable 205 | try: 206 | if not type_hint.__origin__ == collections.abc.Callable: 207 | return False 208 | 209 | except AttributeError: 210 | return False 211 | 212 | # Verify that are is some form of callable.: 213 | # 1) Check if it is either a function or a method 214 | # 2) If it is an object, check if it has a __call__ method. If so use call for checks. 215 | if not (isfunction(arg) or ismethod(arg)): 216 | 217 | try: 218 | arg = getattr(arg, '__call__') 219 | 220 | except AttributeError: 221 | return False 222 | 223 | # Examine signature of given callable 224 | sig_args = signature(arg).parameters 225 | hint_args = type_hint.__args__ 226 | 227 | # Verify Parameter list length 228 | if len(sig_args) != len(hint_args[:-1]): 229 | return False 230 | 231 | return True 232 | -------------------------------------------------------------------------------- /vmbpy/util/scoped_log.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | from functools import wraps 29 | from typing import Any, Callable, Optional, Tuple 30 | 31 | from .log import Log, LogConfig 32 | 33 | __all__ = [ 34 | 'ScopedLogEnable' 35 | ] 36 | 37 | 38 | class _ScopedLog: 39 | __log = Log.get_instance() 40 | 41 | def __init__(self, config: LogConfig): 42 | self.__config: LogConfig = config 43 | self.__old_config: Optional[LogConfig] = None 44 | 45 | def __enter__(self): 46 | self.__old_config = _ScopedLog.__log.get_config() 47 | _ScopedLog.__log.enable(self.__config) 48 | return self 49 | 50 | def __exit__(self, exc_type, exc_value, exc_traceback): 51 | if self.__old_config: 52 | _ScopedLog.__log.enable(self.__old_config) 53 | 54 | else: 55 | _ScopedLog.__log.disable() 56 | 57 | 58 | class ScopedLogEnable: 59 | """Decorator: Enables logging facility before execution of the wrapped function 60 | and disables logging after exiting the wrapped function. This allows more specific 61 | logging of a code section compared to enabling or disabling the global logging mechanism. 62 | 63 | Arguments: 64 | config: 65 | The configuration the log should be enabled with. 66 | """ 67 | def __init__(self, config: LogConfig): 68 | """Add scoped logging to a Callable. 69 | 70 | Arguments: 71 | config: 72 | The configuration the log should be enabled with. 73 | """ 74 | self.__config = config 75 | 76 | def __call__(self, func: Callable[..., Any]): 77 | @wraps(func) 78 | def wrapper(*args: Tuple[Any, ...]): 79 | with _ScopedLog(self.__config): 80 | return func(*args) 81 | 82 | return wrapper 83 | -------------------------------------------------------------------------------- /vmbpy/util/tracer.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | from functools import reduce, wraps 29 | from inspect import signature 30 | from logging import NullHandler 31 | from typing import Any, Callable, TypeVar 32 | 33 | from .log import Log 34 | 35 | __all__ = [ 36 | 'TraceEnable' 37 | ] 38 | 39 | 40 | # TODO: To improve type checking further consider usage of `ParamSpec`. Introduced to standard 41 | # library with Python 3.10 and available as backport in `typing_extensions` package for Python >=3.8 42 | T = TypeVar('T') 43 | 44 | 45 | _FMT_MSG_ENTRY: str = 'Enter | {}' 46 | _FMT_MSG_LEAVE: str = 'Leave | {}' 47 | _FMT_MSG_RAISE: str = 'Raise | {}, {}' 48 | _FMT_ERROR: str = 'ErrorType: {}, ErrorValue: {}' 49 | _INDENT_PER_LEVEL: str = ' ' 50 | 51 | 52 | def _args_to_str(func, *args, **kwargs) -> str: 53 | # Expand function signature 54 | sig = signature(func).bind(*args, **kwargs) 55 | sig.apply_defaults() 56 | full_args = sig.arguments 57 | 58 | # Early return if there is nothing to print 59 | if not full_args: 60 | return '(None)' 61 | 62 | def fold(args_as_str: str, arg): 63 | name, value = arg 64 | 65 | if name == 'self': 66 | arg_str = 'self' 67 | 68 | else: 69 | arg_str = str(value) 70 | 71 | return '{}{}, '.format(args_as_str, arg_str) 72 | 73 | return '({})'.format(reduce(fold, full_args.items(), '')[:-2]) 74 | 75 | 76 | def _get_indent(level: int) -> str: 77 | return _INDENT_PER_LEVEL * level 78 | 79 | 80 | def _create_enter_msg(name: str, level: int, args_str: str) -> str: 81 | msg = '{}{}{}'.format(_get_indent(level), name, args_str) 82 | return _FMT_MSG_ENTRY.format(msg) 83 | 84 | 85 | def _create_leave_msg(name: str, level: int, ) -> str: 86 | msg = '{}{}'.format(_get_indent(level), name) 87 | return _FMT_MSG_LEAVE.format(msg) 88 | 89 | 90 | def _create_raise_msg(name: str, level: int, exc_type: Exception, exc_value: str) -> str: 91 | msg = '{}{}'.format(_get_indent(level), name) 92 | exc = _FMT_ERROR.format(exc_type, exc_value) 93 | return _FMT_MSG_RAISE.format(msg, exc) 94 | 95 | 96 | class _Tracer: 97 | __log = Log.get_instance() 98 | __level: int = 0 99 | 100 | @staticmethod 101 | def is_log_enabled() -> bool: 102 | # If there are any handler registered that are not `NullHandler`s logging is considered to 103 | # be enabled 104 | return not all([isinstance(handler, NullHandler) for handler in _Tracer.__log.handlers]) 105 | 106 | def __init__(self, func, *args, **kwargs): 107 | self.__full_name: str = '{}.{}'.format(func.__module__, func.__qualname__) 108 | self.__full_args: str = _args_to_str(func, *args, **kwargs) 109 | 110 | def __enter__(self): 111 | msg = _create_enter_msg(self.__full_name, _Tracer.__level, self.__full_args) 112 | 113 | _Tracer.__log.trace(msg) 114 | _Tracer.__level += 1 115 | 116 | def __exit__(self, exc_type, exc_value, exc_traceback): 117 | _Tracer.__level -= 1 118 | 119 | if exc_type: 120 | msg = _create_raise_msg(self.__full_name, _Tracer.__level, exc_type, exc_value) 121 | 122 | else: 123 | msg = _create_leave_msg(self.__full_name, _Tracer.__level) 124 | 125 | # TODO: when only python versions >=3.8 are supported, add stacklevel parameter here to 126 | # report correct line numbers in trace logs 127 | _Tracer.__log.trace(msg) 128 | 129 | 130 | class TraceEnable: 131 | """Decorator: Adds an entry of LogLevel. Trace on entry and exit of the wrapped function. 132 | On exit, the log entry contains information if the function was left normally or with an 133 | exception. 134 | """ 135 | 136 | def __call__(self, func: Callable[..., T]) -> Callable[..., T]: 137 | @wraps(func) 138 | def wrapper(*args: Any, **kwargs: Any) -> T: 139 | if _Tracer.is_log_enabled(): 140 | with _Tracer(func, *args, **kwargs): 141 | result = func(*args, **kwargs) 142 | 143 | return result 144 | 145 | else: 146 | return func(*args, **kwargs) 147 | 148 | return wrapper 149 | -------------------------------------------------------------------------------- /vmbpy/util/vmb_enum.py: -------------------------------------------------------------------------------- 1 | """BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Allied Vision Technologies GmbH 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | import enum 28 | 29 | __all__ = [ 30 | 'VmbIntEnum', 31 | 'VmbFlagEnum' 32 | ] 33 | 34 | 35 | class VmbIntEnum(enum.IntEnum): 36 | __str__ = enum.Enum.__str__ 37 | 38 | 39 | class VmbFlagEnum(enum.IntFlag): 40 | __str__ = enum.Enum.__str__ 41 | --------------------------------------------------------------------------------