Camera helpers ============== Every camera driver built on :py:class:`~herosdevices.core.templates.camera.CameraTemplate` can offer five properties. Each reads and writes the active configuration. Exposure time, binning and frame rate have the same units for all drivers. The ROI uses the pixels of the vendor API and the gain the native unit of the driver, see :ref:`camera-roi-units` and :ref:`camera-gain-frame-rate`. .. list-table:: :header-rows: 1 * - Property - Type - Unit * - :py:attr:`~herosdevices.core.templates.camera.CameraTemplate.exposure_time` - ``float`` - seconds * - :py:attr:`~herosdevices.core.templates.camera.CameraTemplate.roi_coordinates` - ``(x_offset, y_offset, width, height)`` - pixels of the vendor API, see :ref:`camera-roi-units` * - :py:attr:`~herosdevices.core.templates.camera.CameraTemplate.binning` - ``(horizontal, vertical)`` - pixel factors * - :py:attr:`~herosdevices.core.templates.camera.CameraTemplate.gain` - ``float`` - driver specific, see :ref:`camera-gain-frame-rate` * - :py:attr:`~herosdevices.core.templates.camera.CameraTemplate.frame_rate` - ``float`` or ``None`` - Hz, ``None`` removes the limit The ROI and binning getters return a :py:class:`tuple` or ``None``, also if the stored configuration holds a list (for example from a JSON file). Reading ------- A getter returns ``None`` if the value is not set in the active configuration or if the driver does not support the feature. :py:meth:`~herosdevices.core.templates.camera.CameraTemplate.supported_features` tells the cases apart. It returns the sorted names of the properties the driver supports. Some getters return ``None`` for stored values they cannot interpret, for example a DCAM binning mode that is not a symmetric factor of 1, 2 or 4. :py:meth:`~herosdevices.core.templates.acq_device.AcquisitionDeviceTemplate.get_available_options` is the second introspection call. It lists the raw device and configuration keys, which are the keys to use in a configuration dict. ``supported_features()`` lists which helper properties the driver implements. On GenICam drivers the helpers write keys that ``get_available_options()`` also lists, for example ``Gain`` and ``AcquisitionFrameRate``. Drivers whose vendor API cannot enumerate options raise :py:class:`NotImplementedError`, for example :py:class:`~herosdevices.hardware.teledyne.pvcam.PvcamCamera`. Writing ------- A setter computes a configuration patch, merges it into the active configuration and calls :py:meth:`~herosdevices.core.templates.camera.CameraTemplate.configure`. It fails in these ways: - The acquisition is running: :py:class:`RuntimeError`. Nothing is changed. Stop the acquisition and set again. - ``_set_config`` returns ``False`` or raises: the previous configuration is restored and the device is re-configured with it on a best-effort basis. A returned ``False`` raises :py:class:`RuntimeError`, an exception from the driver is re-raised. If the re-configuration fails too, the error is logged and the hardware may be partially configured. - The driver does not support the feature: :py:class:`~herosdevices.helper.FeatureNotSupportedError`, a subclass of :py:class:`NotImplementedError`. Nothing is changed. - The driver supports the feature but the camera model has no node for it: the same error, naming the node. The previous configuration is restored. The IDS driver, for example, checks this. - The value is invalid for the driver: :py:class:`ValueError`. Nothing is changed. Examples are asymmetric DCAM binning or a PVCam exposure time that is not a whole number of microseconds. - The patched configuration lacks a required config key: :py:class:`ValueError`. Nothing is changed. Config key order ~~~~~~~~~~~~~~~~ Most drivers apply the configuration in key order, for example the IDS driver, and the valid range of some properties depends on others. A setter moves the keys of its patch to the end of the stored configuration of the active name, in patch order. Keys that are also in ``default_config_dict`` keep their position in :py:meth:`~herosdevices.core.templates.acq_device.AcquisitionDeviceTemplate.get_configuration`, because the defaults are merged first. Example ------- .. code-block:: python from herosdevices.hardware.dummy.cameras import CameraDummy camera = CameraDummy(config_dict={"default": {"exposure_time": 0.01}}) camera.supported_features() camera.roi_coordinates # None, no ROI configured camera.roi_coordinates = (0, 0, 256, 128) camera.binning = (2, 2) camera.roi_coordinates # (0, 0, 256, 128) Writing a driver ---------------- The ten hooks ``_get_*``/``_set_*`` for exposure time, ROI, binning, gain and frame rate are optional. A feature counts as supported if the subclass overrides both hooks of its pair. A pair with only one hook overridden is unsupported, and a warning is logged the first time ``supported_features()`` is called for the class. The property still calls the overridden hook, while the other direction raises :py:class:`~herosdevices.helper.FeatureNotSupportedError`. - A ``_get_*`` hook reads the value from the stored configuration only (``self.get_configuration()``), never from the device. It may return ``None``. - A ``_set_*`` hook only returns a configuration patch. It does not write to the device or change ``self._config``. The driver applies the patch in ``_set_config``. - A hook that needs another value, such as the binning for a ROI conversion, reads it from the configuration instead of calling the other hook, which may be unsupported. - If the device model may lack the node of a helper, ``_set_config`` raises :py:class:`~herosdevices.helper.FeatureNotSupportedError` for it. While a setter applies its patch, the patch keys are in ``self._patch_keys``, so the driver can raise only for these keys and handle a missing node for a key from the user configuration as before. The setter restores the configuration and passes the error on. To add a feature, add to :py:class:`~herosdevices.core.templates.camera.CameraTemplate` the pair of hooks with the default body ``raise FeatureNotSupportedError(...)`` and the property. The getter returns ``None`` on :py:class:`~herosdevices.helper.FeatureNotSupportedError`. The setter passes the hook result to ``_apply_config_patch``. Add the pair to ``_FEATURE_HOOKS``. In ``tests/hardware/test_camera_helpers.py``, add a test value to ``FEATURE_VALUES`` and the feature to the expected features of each driver that supports it. .. _camera-roi-units: ROI units --------- :py:attr:`~herosdevices.core.templates.camera.CameraTemplate.roi_coordinates` is ``(x_offset, y_offset, width, height)`` in the pixels the vendor API uses for its ROI. Some APIs count binned pixels once the binning is larger than 1, for example the GenICam nodes ``OffsetX``, ``OffsetY``, ``Width`` and ``Height``. The same values then cover a different area of the sensor after the binning changes. Other APIs count unbinned sensor pixels. Check the vendor documentation of the camera. .. _camera-gain-frame-rate: Gain and frame rate ------------------- The gain has no common unit. Each driver passes the value to its vendor API unchanged, for example to the GenICam ``Gain`` node. The setter raises a :py:class:`ValueError` for NaN, infinity, booleans and non-numbers. Range checks and side effects, such as switching automatic gain off, depend on the driver and are described in its class documentation. ``frame_rate`` is a limit, not a measurement. The camera may deliver fewer frames if the exposure time, the ROI or the interface bandwidth do not allow the rate. Setting ``None`` removes the limit. The getter returns ``None`` for a removed limit and also if no frame rate is configured or the driver does not support the feature. Use :py:meth:`~herosdevices.core.templates.camera.CameraTemplate.supported_features` to tell unsupported from unlimited. How a driver removes the limit depends on the vendor API, for example the IDS driver writes the maximum of the ``AcquisitionFrameRate`` node. The exposure time must not exceed the frame period. A driver that retries rejected keys after the others, for example the IDS driver, accepts ``ExposureTime`` and ``AcquisitionFrameRate`` in either order. The setter raises a :py:class:`ValueError` for a frame rate that is not positive and finite. ``None`` is allowed.