from enum import Enum, IntEnum
import signal
import struct
import pkg_resources
# hailo_platform package has been renamed to hailort, but the import is still hailo_platform
__version__ = pkg_resources.get_distribution("hailort").version

import sys

from argparse import ArgumentTypeError
import numpy
import time
from hailo_platform.common.logger.logger import default_logger
import gc
import os

import hailo_platform.pyhailort._pyhailort as _pyhailort
if _pyhailort.__version__ != __version__:
    raise ImportError("_pyhailort version ({}) does not match pyhailort version ({})".format(_pyhailort.__version__, __version__))

from hailo_platform.pyhailort._pyhailort import (TemperatureInfo, # noqa F401
                                                 DvmTypes, PowerMeasurementTypes,  # noqa F401
                                                 PowerMeasurementData, NotificationId,  # noqa F401
                                                 OvercurrentAlertState,
                                                 FormatOrder,
                                                 AveragingFactor, SamplingPeriod, MeasurementBufferIndex,
                                                 FormatType, WatchdogMode,
                                                 MipiDataTypeRx, MipiPixelsPerClock,
                                                 MipiClockSelection, MipiIspImageInOrder,
                                                 MipiIspImageOutDataType, IspLightFrequency,
                                                 BootSource, HailoSocketDefs, Endianness,
                                                 MipiInputStreamParams, SensorConfigTypes)

BBOX_PARAMS = _pyhailort.HailoRTDefaults.BBOX_PARAMS()
HAILO_DEFAULT_ETH_CONTROL_PORT = _pyhailort.HailoRTDefaults.HAILO_DEFAULT_ETH_CONTROL_PORT()
INPUT_DATAFLOW_BASE_PORT = _pyhailort.HailoRTDefaults.DEVICE_BASE_INPUT_STREAM_PORT()
OUTPUT_DATAFLOW_BASE_PORT = _pyhailort.HailoRTDefaults.DEVICE_BASE_OUTPUT_STREAM_PORT()
PCIE_ANY_DOMAIN = _pyhailort.HailoRTDefaults.PCIE_ANY_DOMAIN()
HAILO_UNIQUE_VDEVICE_GROUP_ID = _pyhailort.HailoRTDefaults.HAILO_UNIQUE_VDEVICE_GROUP_ID()
DEFAULT_VSTREAM_TIMEOUT_MS = 10000
DEFAULT_VSTREAM_QUEUE_SIZE = 2

class HailoSocket(object):
    MAX_UDP_PAYLOAD_SIZE = HailoSocketDefs.MAX_UDP_PAYLOAD_SIZE()
    MIN_UDP_PAYLOAD_SIZE = HailoSocketDefs.MIN_UDP_PAYLOAD_SIZE()
    MAX_UDP_PADDED_PAYLOAD_SIZE = HailoSocketDefs.MAX_UDP_PADDED_PAYLOAD_SIZE()
    MIN_UDP_PADDED_PAYLOAD_SIZE = HailoSocketDefs.MIN_UDP_PADDED_PAYLOAD_SIZE()
    MAX_ALIGNED_UDP_PAYLOAD_SIZE_RTP = HailoSocketDefs.MAX_ALIGNED_UDP_PAYLOAD_SIZE_RTP()

class HailoSchedulingAlgorithm(_pyhailort.SchedulingAlgorithm):
    pass

class HailoRTException(Exception):
    pass

class UdpRecvError(HailoRTException):
    pass

class InvalidProtocolVersionException(HailoRTException):
    pass

class HailoRTFirmwareControlFailedException(HailoRTException):
    pass

class HailoRTInvalidFrameException(HailoRTException):
    pass

class HailoRTUnsupportedOpcodeException(HailoRTException):
    pass

class HailoRTTimeout(HailoRTException):
    pass

class HailoRTStreamAborted(HailoRTException):
    pass

class HailoRTStreamAbortedByUser(HailoRTException):
    pass

class HailoRTInvalidOperationException(HailoRTException):
    pass

class HailoRTInvalidArgumentException(HailoRTException):
    pass

class HailoRTNotFoundException(HailoRTException):
    pass

class HailoRTInvalidHEFException(HailoRTException):
    pass

class HailoRTEthException(HailoRTException):
    pass

class HailoRTPCIeDriverException(HailoRTException):
    pass

class HailoRTNetworkGroupNotActivatedException(HailoRTException):
    pass

class HailoStatusInvalidValueException(Exception):
    pass

class ExceptionWrapper(object):
    def __enter__(self):
        pass

    def __exit__(self, exception_type, value, traceback):
        if value is not None:
            if exception_type is _pyhailort.HailoRTStatusException:
                self._raise_indicative_status_exception(value)
            else:
                raise

    def _raise_indicative_status_exception(self, libhailort_exception):
        error_code = int(libhailort_exception.args[0])
        string_error_code = get_status_message(error_code)
        if string_error_code == "HAILO_ETH_RECV_FAILURE":
            raise UdpRecvError("Failed to receive data") from libhailort_exception
        if string_error_code == "HAILO_UNSUPPORTED_CONTROL_PROTOCOL_VERSION":
            raise InvalidProtocolVersionException("HailoRT has failed because an invalid protocol version was received from device") from libhailort_exception
        if string_error_code == "HAILO_FW_CONTROL_FAILURE":
            raise HailoRTFirmwareControlFailedException("libhailort control operation failed") from libhailort_exception
        if string_error_code == "HAILO_UNSUPPORTED_OPCODE":
            raise HailoRTUnsupportedOpcodeException("HailoRT has failed because an unsupported opcode was sent to device") from libhailort_exception
        if string_error_code == "HAILO_INVALID_FRAME":
            raise HailoRTInvalidFrameException("An invalid frame was received") from libhailort_exception
        if string_error_code == "HAILO_TIMEOUT":
            raise HailoRTTimeout("Received a timeout - hailort has failed because a timeout had occurred") from libhailort_exception
        if string_error_code == "HAILO_STREAM_ABORTED_BY_HW":
            raise HailoRTStreamAborted("Stream aborted due to an external event") from libhailort_exception
        if string_error_code == "HAILO_STREAM_ABORTED_BY_USER":
            raise HailoRTStreamAbortedByUser("Stream was aborted by user") from libhailort_exception

        if string_error_code == "HAILO_INVALID_OPERATION":
            raise HailoRTInvalidOperationException("Invalid operation. See hailort.log for more information") from libhailort_exception
        if string_error_code == "HAILO_INVALID_ARGUMENT":
            raise HailoRTInvalidArgumentException("Invalid argument. See hailort.log for more information") from libhailort_exception
        if string_error_code == "HAILO_NOT_FOUND":
            raise HailoRTNotFoundException("Item not found. See hailort.log for more information") from libhailort_exception

        if string_error_code == "HAILO_INVALID_HEF":
            raise HailoRTInvalidHEFException("Invalid HEF. See hailort.log for more information") from libhailort_exception

        if string_error_code == "HAILO_ETH_FAILURE":
            raise HailoRTEthException("Ethernet failure. See hailort.log for more information") from libhailort_exception
        if string_error_code == "HAILO_DRIVER_FAIL":
            raise HailoRTPCIeDriverException("PCIe driver failure. run 'dmesg | grep hailo' for more information") from libhailort_exception

        if string_error_code == "HAILO_NETWORK_GROUP_NOT_ACTIVATED":
            raise HailoRTNetworkGroupNotActivatedException("Network group is not activated") from libhailort_exception
        else:
            raise HailoRTException("libhailort failed with error: {} ({})".format(error_code, string_error_code)) from libhailort_exception

def get_status_message(status_code):
    status_str = _pyhailort.get_status_message(status_code)
    if status_str == "":
        raise HailoStatusInvalidValueException("Value {} is not a valid status".format(status_code))
    return status_str


class HailoUdpScan(object):
    def __init__(self):
        self._logger = default_logger()
        with ExceptionWrapper():
            self._scan = _pyhailort.UdpScan()

    def scan_devices(self, interface_name, timeout_seconds=3):
        self._logger.info('Scanning over interface {iface}'.format(iface=interface_name))
        timeout_milliseconds = int(timeout_seconds * 1000)
        device_ip_addresses =  self._scan.scan_devices(interface_name, timeout_milliseconds)
        for ip in device_ip_addresses:
            self._logger.debug("Found board at: {}".format(ip))
        return device_ip_addresses


class NetworkRateLimiter(object):
    def __init__(self, ip, port, rate_bytes_per_sec):
        if sys.platform != 'linux':
            raise HailoRTInvalidOperationException('NetworkRateLimiter is supported only on UNIX os')
        self._ip = ip
        self._port = port
        self._rate_bytes_per_sec = rate_bytes_per_sec

    def set_rate_limit(self):
        with ExceptionWrapper():
            return _pyhailort.NetworkRateLimiter.set_rate_limit(self._ip, self._port, self._rate_bytes_per_sec)
    
    def reset_rate_limit(self):
        with ExceptionWrapper():
            return _pyhailort.NetworkRateLimiter.reset_rate_limit(self._ip, self._port)

    def get_interface_name(ip):
        "get the interface corresponding to the given ip"
        with ExceptionWrapper():
            return _pyhailort.NetworkRateLimiter.get_interface_name(ip)


class ConfigureParams(object):

    @staticmethod
    def create_from_hef(hef, interface):
        """Create configure params from HEF. These params affects the HEF configuration into a device.

        Args:
            hef (:class:`HEF`): The HEF to create the parameters from.
            interface (:class:`HailoStreamInterface`): The stream_interface to create stream_params for.

        Returns:
            dict: The created stream params. The keys are the network_group names in the HEF. The values are default params, which can be changed.
        """
        with ExceptionWrapper():
            return hef._hef.create_configure_params(interface)

    @staticmethod
    def create_mipi_inputs_from_hef(hef, output_interface, mipi_rx_id=0, data_type=MipiDataTypeRx.RAW_8,
            img_width_pixels=1920, img_height_pixels=1080,
            pixels_per_clock=MipiPixelsPerClock.PIXELS_PER_CLOCK_4, number_of_lanes=2,
            clock_selection=MipiClockSelection.SELECTION_AUTOMATIC, data_rate=260, virtual_channel_index=0,
            isp_enable=False, isp_img_in_order=MipiIspImageInOrder.GR_FIRST,
            isp_img_out_data_type=MipiIspImageOutDataType.RGB_888, isp_crop_enable=False,
            isp_crop_output_width_pixels=1920, isp_crop_output_height_pixels=1080,
            isp_crop_output_width_start_offset_pixels=0, isp_crop_output_height_start_offset_pixels=0,
            isp_test_pattern_enable=True, isp_configuration_bypass=False,
            isp_run_time_ae_enable=True, isp_run_time_awb_enable=True, isp_run_time_adt_enable=True,
            isp_run_time_af_enable=False, isp_run_time_calculations_interval_ms=0,
            isp_light_frequency=IspLightFrequency.LIGHT_FREQ_50_HZ):
        """Create configure params from HEF. These params affects the HEF configuration into a device.

        .. attention:: The ISP and its features are not officially supported yet.

        Args:
            hef (:class:`HEF`): The HEF to create the parameters from.
            output_interface (:class:`HailoStreamInterface`): The stream_interface to create output stream_params for.
            mipi_rx_id (int): Selection of which MIPI Rx device to use.
            data_type (:class:`~hailo_platform.pyhailort.pyhailort.MipiDataTypeRx`): The data type which will be passed over the MIPI.
            img_width_pixels (int): The width in pixels of the image that enter to the mipi CSI. The sensor output.
                                        When isp_enable and isp_crop_enable is false, is also the stream input.
            img_height_pixels (int): The height in pixels of the image that enter to the mipi CSI. The sensor output.
                                        When isp_enable and isp_crop_enable is false, is also the stream input.
            pixels_per_clock (:class:`~hailo_platform.pyhailort.pyhailort.MipiPixelsPerClock`): Number of pixels transmitted at each
                clock.
            number_of_lanes (int): Number of lanes to use.
            clock_selection (:class:`~hailo_platform.pyhailort.pyhailort.MipiClockSelection`): Selection of clock range that would be
                used. Setting :class:`~hailo_platform.pyhailort.pyhailort.MipiClockSelection.SELECTION_AUTOMATIC` means that the
                clock selection is calculated from the data rate.
            data_rate (int): Rate of the passed data (MHz).
            virtual_channel_index (int): The virtual channel index of the MIPI dphy.
            isp_enable (bool): Enable the ISP block in the MIPI dataflow. The ISP is not supported yet.
            isp_img_in_order (:class:`~hailo_platform.pyhailort.pyhailort.MipiIspImageInOrder`):
                The ISP Rx bayer pixel order. Only relevant when the ISP is enabled.
            isp_img_out_data_type (:class:`~hailo_platform.pyhailort.pyhailort.MipiIspImageOutDataType`):
                The data type that the mipi will take out. Only relevant when the ISP is enabled.
            isp_crop_enable (bool): Enable the crop feature in the ISP. Only relevant when the ISP is enabled.
            isp_crop_output_width_pixels (int): The width in pixels of the output window that the ISP take out. The stream input.
                                        Useful when isp_crop_enable is True. Only relevant when the ISP is enabled.
            isp_crop_output_height_pixels (int): The height in pixels of the output window that the ISP take out. The stream input.
                                        Useful when isp_crop_enable is True. Only relevant when the ISP is enabled.
            isp_crop_output_width_start_offset_pixels (int): The width start point of the output window that the ISP take out. 
                                        Useful when isp_crop_enable is True. Only relevant when the ISP is enabled.
            isp_crop_output_height_start_offset_pixels (int): The height start point of the output window that the ISP take out. 
                                        Useful when isp_crop_enable is True. Only relevant when the ISP is enabled.
            isp_test_pattern_enable (bool): Enable Test pattern from the ISP. Only relevant when the ISP is enabled.
            isp_configuration_bypass (bool): Don't load the ISP configuration file from the FLASH. Only relevant when the ISP is enabled.
            isp_run_time_ae_enable (bool): Enable the run-time Auto Exposure in the ISP. Only relevant when the ISP is enabled.
            isp_run_time_awb_enable (bool): Enable the run-time Auto White Balance in the ISP. Only relevant when the ISP is enabled.
            isp_run_time_adt_enable (bool): Enable the run-time Adaptive Function in the ISP. Only relevant when the ISP is enabled.
            isp_run_time_af_enable (bool): Enable the run-time Auto Focus in the ISP. Only relevant when the ISP is enabled.
            isp_run_time_calculations_interval_ms (int): Interval in milliseconds between ISP run time calculations. Only relevant when the ISP is enabled.
            isp_light_frequency (:class:`~hailo_platform.pyhailort.pyhailort.IspLightFrequency`):
                                        Selection of the light frequency. This parameter varies depending on the power grid of the country where 
                                        the product is running. Only relevant when the ISP is enabled.
        Returns:
            dict: The created stream params. The keys are the network_group names in the HEF. The values are default params, which can be changed.
        """

        mipi_params = MipiInputStreamParams()
        mipi_params.mipi_rx_id = mipi_rx_id
        mipi_params.data_type = data_type
        mipi_params.isp_enable = isp_enable
        mipi_params.mipi_common_params.pixels_per_clock = pixels_per_clock
        mipi_params.mipi_common_params.number_of_lanes = number_of_lanes
        mipi_params.mipi_common_params.clock_selection = clock_selection
        mipi_params.mipi_common_params.virtual_channel_index = virtual_channel_index
        mipi_params.mipi_common_params.data_rate = data_rate
        mipi_params.mipi_common_params.img_width_pixels = img_width_pixels
        mipi_params.mipi_common_params.img_height_pixels = img_height_pixels
        mipi_params.isp_params.img_in_order = isp_img_in_order
        mipi_params.isp_params.img_out_data_type = isp_img_out_data_type
        mipi_params.isp_params.crop_enable = isp_crop_enable
        mipi_params.isp_params.crop_output_width_pixels = isp_crop_output_width_pixels
        mipi_params.isp_params.crop_output_height_pixels = isp_crop_output_height_pixels
        mipi_params.isp_params.crop_output_width_start_offset_pixels = isp_crop_output_width_start_offset_pixels
        mipi_params.isp_params.crop_output_height_start_offset_pixels = isp_crop_output_height_start_offset_pixels
        mipi_params.isp_params.test_pattern_enable = isp_test_pattern_enable
        mipi_params.isp_params.configuration_bypass = isp_configuration_bypass
        mipi_params.isp_params.run_time_ae_enable = isp_run_time_ae_enable
        mipi_params.isp_params.run_time_awb_enable = isp_run_time_awb_enable
        mipi_params.isp_params.run_time_adt_enable = isp_run_time_adt_enable
        mipi_params.isp_params.run_time_af_enable = isp_run_time_af_enable
        mipi_params.isp_params.isp_run_time_calculations_interval_ms = isp_run_time_calculations_interval_ms
        mipi_params.isp_params.isp_light_frequency = isp_light_frequency
        with ExceptionWrapper():
            return hef._hef.create_configure_params_mipi_input(output_interface, mipi_params)

def _get_name_as_str(name):
    return name if name is not None else ""

class HEF(object):
    """Python representation of the Hailo Executable Format, which contains one or more compiled
    models.
    """

    def __init__(self, hef_source):
        """Constructor for the HEF class.

        Args:
            hef_source (str or bytes): The source from which the HEF object will be created. If the
                source type is `str`, it is treated as a path to an hef file. If the source type is
                `bytes`, it is treated as a buffer. Any other type will raise a ValueError.
        """

        with ExceptionWrapper():
            if isinstance(hef_source, str):
                self._hef = _pyhailort.Hef.create_from_file(hef_source)
                self._path = hef_source
            elif isinstance(hef_source, bytes):
                self._hef = _pyhailort.Hef.create_from_buffer(hef_source)
                self._path = None
            else: 
                raise ValueError("HEF can only be created from a file path (str) or a buffer (bytes)")
        self._sorted_output_names = {}

    def get_networks_names(self, network_group_name=None):
        """Gets the names of all networks in a specific network group.

        Args:
            network_group_name (str, optional): The name of the network group to access. If not given, first network_group is addressed.

        Returns:
            list of str: The names of the networks.
        """
        name = _get_name_as_str(network_group_name)
        with ExceptionWrapper():
            return self._hef.get_networks_names(name)

    @property
    def path(self):
        """HEF file path."""
        return self._path
    
    def get_network_group_names(self):
        """Get the names of the network groups in this HEF."""
        with ExceptionWrapper():
            return self._hef.get_network_group_names()
  
    def get_network_groups_infos(self):
        """Get information about the network groups in this HEF."""
        with ExceptionWrapper():
            return self._hef.get_network_groups_infos()

    def get_input_vstream_infos(self, name=None):
        """Get input vstreams information.

        Args:
            name (str, optional): The name of the network or network_group to access. In case network_group name is given,
                Address all networks of the given network_group. In case not given, first network_group is addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input vstreams.
        """
        name = _get_name_as_str(name)
        return self._hef.get_input_vstream_infos(name)

    def get_output_vstream_infos(self, name=None):
        """Get output vstreams information.

        Args:
            name (str, optional): The name of the network or network_group to access. In case network_group name is given,
                Address all networks of the given network_group. In case not given, first network_group is addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all output vstreams
        """
        name = _get_name_as_str(name)
        return self._hef.get_output_vstream_infos(name)

    def get_all_vstream_infos(self, name=None):
        """Get input and output vstreams information.

        Args:
            name (str, optional): The name of the network or network_group to access. In case network_group name is given,
                Address all networks of the given network_group. In case not given, first network_group is addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input and output vstreams
        """
        name = _get_name_as_str(name)
        return self._hef.get_all_vstream_infos(name)

    def get_input_stream_infos(self, name=None):
        """Get the input low-level streams information.

        Args:
            name (str, optional): The name of the network or network_group to access. In case network_group name is given,
                Address all networks of the given network_group. In case not given, first network_group is addressed.

        Returns:
            List of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with information objects
            of all input low-level streams.
        """
        name = _get_name_as_str(name)
        return self._hef.get_input_stream_infos(name)


    def get_output_stream_infos(self, name=None):
        """Get the output low-level streams information of a specific network group.

        Args:
            name (str, optional): The name of the network or network_group to access. In case network_group name is given,
                Address all networks of the given network_group. In case not given, first network_group is addressed.

        Returns:
            List of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with information objects
            of all output low-level streams.
        """
        name = _get_name_as_str(name)
        return self._hef.get_output_stream_infos(name)

    def get_all_stream_infos(self, name=None):
        """Get input and output streams information of a specific network group.

        Args:
            name (str, optional): The name of the network or network_group to access. In case network_group name is given,
                Address all networks of the given network_group. In case not given, first network_group is addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with all the information objects of all input and output streams
        """
        name = _get_name_as_str(name)
        return self._hef.get_all_stream_infos(name)

    def get_sorted_output_names(self, network_group_name=None):
        """Get the names of the outputs in a network group. The order of names is determined by
        the SDK. If the network group is not given, the first one is used.
        """
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]

        if network_group_name not in self._sorted_output_names:
            with ExceptionWrapper():
                self._sorted_output_names[network_group_name] = self._hef.get_sorted_output_names(network_group_name)
        return self._sorted_output_names[network_group_name]

    def bottleneck_fps(self, network_group_name=None):
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]
        with ExceptionWrapper():
            bottleneck_fps = self._hef.get_bottleneck_fps(network_group_name)
            if bottleneck_fps == 0:
                raise HailoRTException("bottleneck_fps is zero")
            return bottleneck_fps

    def get_udp_rates_dict(self, fps, max_supported_rate_bytes, network_group_name=None):
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]
        with ExceptionWrapper():
            return self._hef.get_udp_rates_dict(network_group_name, fps, int(max_supported_rate_bytes))

    def get_vstream_name_from_original_name(self, original_name, network_group_name=None):
        """Get vstream name from original layer name for a specific network group.

        Args:
            original_name (str): The original layer name.
            network_group_name (str, optional): The name of the network group to access. If not given, first network_group is addressed.

        Returns:
            str: the matching vstream name for the provided original name.
        """
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]
        with ExceptionWrapper():
            return self._hef.get_vstream_name_from_original_name(original_name, network_group_name)

    def get_original_names_from_vstream_name(self, vstream_name, network_group_name=None):
        """Get original names list from vstream name for a specific network group.

        Args:
            vstream_name (str): The stream name.
            network_group_name (str, optional): The name of the network group to access. If not given, first network_group is addressed.

        Returns:
            list of str: all the matching original layers names for the provided vstream name.
        """
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]
        with ExceptionWrapper():
            return self._hef.get_original_names_from_vstream_name(vstream_name, network_group_name)

    def get_vstream_names_from_stream_name(self, stream_name, network_group_name=None):
        """Get vstream names list from their underlying stream name for a specific network group.

        Args:
            stream_name (str): The underlying stream name.
            network_group_name (str, optional): The name of the network group to access. If not given, first network_group is addressed.

        Returns:
            list of str: All the matching vstream names for the provided stream name.
        """
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]
        with ExceptionWrapper():
            return self._hef.get_vstream_names_from_stream_name(stream_name, network_group_name)

    def get_stream_names_from_vstream_name(self, vstream_name, network_group_name=None):
        """Get stream name from vstream name for a specific network group.

        Args:
            vstream_name (str): The name of the vstreams.
            network_group_name (str, optional): The name of the network group to access. If not given, first network_group is addressed.

        Returns:
            list of str: All the underlying streams names for the provided vstream name.
        """
        if network_group_name is None:
            network_group_name = self.get_network_group_names()[0]
        with ExceptionWrapper():
            return self._hef.get_stream_names_from_vstream_name(vstream_name, network_group_name)


class ConfiguredNetwork(object):
    """Represents a network group loaded to the device."""

    def __init__(self, configured_network):
        self._configured_network = configured_network
        self._input_vstreams_holders = []
        self._output_vstreams_holders = []

    def get_networks_names(self):
        return self._configured_network.get_networks_names()

    def activate(self, network_group_params=None):
        """Activate this network group in order to infer data through it.

        Args:
            network_group_params (:obj:`hailo_platform.pyhailort._pyhailort.ActivateNetworkGroupParams`, optional):
                Network group activation params. If not given, default params will be applied,

        Returns:
            :class:`ActivatedNetworkContextManager`: Context manager that returns the activated
            network group.

        Note:
            Usage of `activate` when scheduler enabled is deprecated. On this case, this function will return None and print deprecation warning.
        """
        if self._configured_network.is_scheduled():
            default_logger().warning("Calls to `activate()` when working with scheduler are deprecated! On future versions this call will raise an error.")
            return EmptyContextManager()

        network_group_params = network_group_params or self.create_params()
        with ExceptionWrapper():
            return ActivatedNetworkContextManager(self,
                self._configured_network.activate(network_group_params))

    def wait_for_activation(self, timeout_ms=None):
        """Block until activated, or until ``timeout_ms`` is passed.

        Args:
            timeout_ms (int, optional): Timeout value in milliseconds to wait for activation.
                Defaults to ``HAILO_INFINITE``.

        Raises:
            :class:`HailoRTTimeout`: In case of timeout.
        """
        MAX_INT = 0x7fffffff
        with ExceptionWrapper():
            if timeout_ms is None:
                timeout_ms = MAX_INT
            return self._configured_network.wait_for_activation(timeout_ms)

    @staticmethod
    def create_params():
        """Create activation params for network_group.

        Returns:
            :obj:`hailo_platform.pyhailort._pyhailort.ActivateNetworkGroupParams`.
        """
        return _pyhailort.ActivateNetworkGroupParams.default()

    @property
    def name(self):
        return self._configured_network.get_name()

    def get_output_shapes(self):
        name_to_shape = {vstream_info.name : vstream_info.shape for vstream_info in self.get_output_vstream_infos()}
        results = []
        for name in self.get_sorted_output_names():
            results.append(name_to_shape[name])
        return tuple(results)

    def get_sorted_output_names(self):
        return self._configured_network.get_sorted_output_names()

    def get_input_vstream_infos(self, network_name=None):
        """Get input vstreams information.

        Args:
            network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input vstreams
        """

        name = network_name if network_name is not None else ""
        return self._configured_network.get_input_vstream_infos(name)

    def get_output_vstream_infos(self, network_name=None):
        """Get output vstreams information.

        Args:
            network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all output vstreams
        """

        name = network_name if network_name is not None else ""
        return self._configured_network.get_output_vstream_infos(name)

    def get_all_vstream_infos(self, network_name=None):
        """Get input and output vstreams information.

        Args:
            network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input and output vstreams
        """

        name = network_name if network_name is not None else ""
        return self._configured_network.get_all_vstream_infos(name)

    def get_input_stream_infos(self, network_name=None):
        """Get the input low-level streams information of a specific network group.

        Args:
            network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed.

        Returns:
            List of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with information objects
            of all input low-level streams.
        """

        name = network_name if network_name is not None else ""
        return self._configured_network.get_input_stream_infos(name)

    def get_output_stream_infos(self, network_name=None):
        """Get the output low-level streams information of a specific network group.

        Args:
            network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed.

        Returns:
            List of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with information objects
            of all output low-level streams.
        """

        name = network_name if network_name is not None else ""
        return self._configured_network.get_output_stream_infos(name)

    def get_all_stream_infos(self, network_name=None):
        """Get input and output streams information of a specific network group.

        Args:
            network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed.

        Returns:
            list of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with all the information objects of all input and output streams
        """

        name = network_name if network_name is not None else ""
        return self._configured_network.get_all_stream_infos(name)

    def get_udp_rates_dict(self, fps, max_supported_rate_bytes):
        with ExceptionWrapper():
            return self._configured_network.get_udp_rates_dict(int(fps), int(max_supported_rate_bytes))

    def _create_input_vstreams(self, input_vstreams_params):
        input_vstreams_holder = self._configured_network.InputVStreams(input_vstreams_params)
        self._input_vstreams_holders.append(input_vstreams_holder)
        return input_vstreams_holder

    def _create_output_vstreams(self, output_vstreams_params):
        output_vstreams_holder = self._configured_network.OutputVStreams(output_vstreams_params)
        self._output_vstreams_holders.append(output_vstreams_holder)
        return output_vstreams_holder

    def get_stream_names_from_vstream_name(self, vstream_name):
        """Get stream name from vstream name for a specific network group.

        Args:
            vstream_name (str): The name of the vstreams.

        Returns:
            list of str: All the underlying streams names for the provided vstream name.
        """
        with ExceptionWrapper():
            return self._configured_network.get_stream_names_from_vstream_name(vstream_name)

    def get_vstream_names_from_stream_name(self, stream_name):
        """Get vstream names list from their underlying stream name for a specific network group.

        Args:
            stream_name (str): The underlying stream name.

        Returns:
            list of str: All the matching vstream names for the provided stream name.
        """
        with ExceptionWrapper():
            return self._configured_network.get_vstream_names_from_stream_name(stream_name)

    def set_scheduler_timeout(self, timeout_ms, network_name=None):
        """Sets the maximum time period that may pass before getting run time from the scheduler,
            even without reaching the minimum required send requests (e.g. threshold - see set_scheduler_threshold()),
            as long as at least one send request has been sent.
            This time period is measured since the last time the scheduler gave this network group run time.
        
        Args:
            timeout_ms (int): Timeout in milliseconds.
        """
        name = network_name if network_name is not None else ""
        return self._configured_network.set_scheduler_timeout(timeout_ms, name)

    def set_scheduler_threshold(self, threshold):
        """Sets the minimum number of send requests required before the network is considered ready to get run time from the scheduler.
            If at least one send request has been sent, but the threshold is not reached within a set time period (e.g. timeout - see hailo_set_scheduler_timeout()),
            the scheduler will consider the network ready regardless.

        Args:
            threshold (int): Threshold in number of frames.
        """
        return self._configured_network.set_scheduler_threshold(threshold)

    def set_scheduler_priority(self, priority):
        """Sets the priority of the network.
            When the model scheduler will choose the next network, networks with higher priority will be prioritized in the selection.
            bigger number represent higher priority.

        Args:
            priority (int): Priority as a number between HAILO_SCHEDULER_PRIORITY_MIN - HAILO_SCHEDULER_PRIORITY_MAX.
        """
        return self._configured_network.set_scheduler_priority(priority)


class EmptyContextManager(object):
    """An empty context manager that returns instead of activated network group when scheduler is enabled`."""

    def __init__(self):
        pass

    def __enter__(self):
        pass

    def __exit__(self, *args):
        pass


class ActivatedNetworkContextManager(object):
    """A context manager that returns the activated network group upon enter."""

    def __init__(self, configured_network, activated_network):
        self._configured_network = configured_network
        self._activated_network = activated_network

    def __enter__(self):
        with ExceptionWrapper():
            activated_network_group = ActivatedNetwork(self._configured_network, self._activated_network.__enter__())
        return activated_network_group
    
    def __exit__(self, *args):
        self._activated_network.__exit__(*args)


class ActivatedNetwork(object):
    """The network group that is currently activated for inference."""

    def __init__(self, configured_network, activated_network):
        self._configured_network = configured_network
        self._activated_network = activated_network
        self._last_number_of_invalid_frames_read = 0

    @property
    def name(self):
        return self._configured_network.name

    def get_number_of_invalid_frames(self, clear=True):
        """Returns number of invalid frames.

        Args:
            clear (bool): If set, the returned value will be the number of invalid frames read since the last call to this function.

        Returns:
            int: Number of invalid frames.
        """
        total_invalid_frames_count = self._activated_network.get_invalid_frames_count()
        if clear:
            value = total_invalid_frames_count - self._last_number_of_invalid_frames_read
        self._last_number_of_invalid_frames_read = total_invalid_frames_count
        return value if clear else total_invalid_frames_count

    def validate_all_frames_are_valid(self):
        """Validates that all of the frames so far are valid (no invalid frames)."""
        number_of_invalid_frames = self.get_number_of_invalid_frames()
        if number_of_invalid_frames != 0:
            raise HailoRTException("There are {} invalid frames.".format(number_of_invalid_frames))

    def get_sorted_output_names(self):
        return self._configured_network.get_sorted_output_names()

    def _get_intermediate_buffer(self, src_context_index, src_stream_index):
        with ExceptionWrapper():
            return self._activated_network.get_intermediate_buffer(src_context_index, src_stream_index)


class InferVStreams(object):
    """Pipeline that allows to call blocking inference, to be used as a context manager."""

    def __init__(self, configured_net_group, input_vstreams_params, output_vstreams_params,
        tf_nms_format=False):
        """Constructor for the InferVStreams class.

        Args:
            configured_net_group (:class:`ConfiguredNetwork`): The configured network group for
                which the pipeline is created.
            input_vstreams_params (dict from str to :class:`InputVStreamParams`): Params for the
                input vstreams in the pipeline. Only members of this dict will take part in the
                inference.
            output_vstreams_params (dict from str to :class:`OutputVStreamParams`): Params for the
                output vstreams in the pipeline. Only members of this dict will take part in the
                inference.
            tf_nms_format (bool, optional): indicates whether the returned nms outputs should be in
                Hailo format or TensorFlow format. Default is False (using Hailo format).

                * Hailo format -- list of :obj:`numpy.ndarray`. Each element represents the
                  detections (bboxes) for the class, and its shape is
                  ``[number_of_detections, BBOX_PARAMS]``
                * TensorFlow format -- :obj:`numpy.ndarray` of shape
                  ``[class_count, BBOX_PARAMS, detections_count]`` padded with empty bboxes.
        """

        self._configured_net_group = configured_net_group
        self._net_group_name = configured_net_group.name
        self._input_vstreams_params = input_vstreams_params
        self._output_vstreams_params = output_vstreams_params
        self._tf_nms_format = tf_nms_format
        self._total_time = None
        self._hw_time = None
        self._network_name_to_outputs = InferVStreams._get_network_to_outputs_mapping(configured_net_group)
        self._input_name_to_network_name = InferVStreams._get_input_name_to_network_mapping(configured_net_group)

    @staticmethod
    def _get_input_name_to_network_mapping(configured_net_group):
        input_name_to_network_mapping = {}
        for network_name in configured_net_group.get_networks_names():
            for input_vstream_info in configured_net_group.get_input_vstream_infos(network_name):
                input_name_to_network_mapping[input_vstream_info.name] = network_name
        return input_name_to_network_mapping

    @staticmethod
    def _get_network_to_outputs_mapping(configured_net_group):
        network_to_outputs_mapping = {}
        for network_name in configured_net_group.get_networks_names():
            network_to_outputs_mapping[network_name] = set()
            for output_vstream_info in configured_net_group.get_output_vstream_infos(network_name):
                network_to_outputs_mapping[network_name].add(output_vstream_info.name)
        return network_to_outputs_mapping

    def _make_output_buffers_and_infos(self, input_data, batch_size):
        output_buffers = {}
        output_buffers_info = {}
        already_seen_networks = set()
        for input_name in input_data.keys():
            network_name = self._input_name_to_network_name[input_name]
            if (network_name not in already_seen_networks) :
                already_seen_networks.add(network_name)
                output_vstream_infos = self._configured_net_group.get_output_vstream_infos()
                for output_name in self._network_name_to_outputs[network_name]:
                    output_buffers_info[output_name] = OutputLayerUtils(output_vstream_infos, output_name, self._infer_pipeline,
                        self._net_group_name)
                    output_tensor_info = output_buffers_info[output_name].output_tensor_info
                    shape, dtype = output_tensor_info
                    if (output_buffers_info[output_name].output_order == FormatOrder.HAILO_NMS_WITH_BYTE_MASK):
                        # Note: In python bindings the output data gets converted to py::array with dtype=dtype.
                        #   In `HAILO_NMS_WITH_BYTE_MASK` we would like to get the data as uint8 and convert it by it's format.
                        #   Therefore we need to get it as uint8 instead of float32 and adjust the shape size.
                        dtype = numpy.uint8
                        shape[0] = shape[0] * 4
                    output_buffers[output_name] = numpy.empty([batch_size] + list(shape), dtype=dtype)
        return output_buffers, output_buffers_info

    def __enter__(self):
        self._infer_pipeline = _pyhailort.InferVStreams(self._configured_net_group._configured_network,
            self._input_vstreams_params, self._output_vstreams_params)
        return self
    
    def infer(self, input_data):
        """Run inference on the hardware device.

        Args:
            input_data (dict of :obj:`numpy.ndarray`): Where the key is the name of the input_layer,
                and the value is the data to run inference on.

        Returns:
            dict: Output tensors of all output layers. The keys are outputs names and the values
            are output data tensors as :obj:`numpy.ndarray` (or list of :obj:`numpy.ndarray` in case of nms output and tf_nms_format=False).
        """

        time_before_infer_calcs = time.perf_counter()
        if not isinstance(input_data, dict):
            input_stream_infos = self._configured_net_group.get_input_stream_infos()
            if len(input_stream_infos) != 1:
                raise Exception("when there is more than one input, the input_data should be of type dict,"
                                             " mapping between each input_name, and his input_data tensor. number of inputs: {}".format(len(input_stream_infos)))
            input_data = {input_stream_infos[0].name : input_data}

        batch_size = InferVStreams._get_number_of_frames(input_data)
        output_buffers, output_buffers_info = self._make_output_buffers_and_infos(input_data, batch_size)

        for input_layer_name in input_data:
            # TODO: Remove cast after tests are updated and are working
            self._cast_input_data_if_needed(input_layer_name, input_data)
            self._validate_input_data_format_type(input_layer_name, input_data)
            self._make_c_contiguous_if_needed(input_layer_name, input_data)

        with ExceptionWrapper():
            time_before_infer = time.perf_counter()
            self._infer_pipeline.infer(input_data, output_buffers, batch_size)
            self._hw_time = time.perf_counter() - time_before_infer

        for name, result_array in output_buffers.items():
            # TODO: HRT-11726 - Combine Pyhailort NMS and NMS_WITH_BYTE_MASK decoding function
            if output_buffers_info[name].output_order == FormatOrder.HAILO_NMS_WITH_BYTE_MASK:
                nms_shape = output_buffers_info[name].vstream_info.nms_shape
                output_dtype = output_buffers_info[name].output_dtype
                input_stream_infos = self._configured_net_group.get_input_stream_infos()
                if len(input_stream_infos) != 1:
                    raise Exception("Output format HAILO_NMS_WITH_BYTE_MASK should have 1 input. Number of inputs: {}".format(len(input_stream_infos)))
                input_height = input_stream_infos[0].shape[0]
                input_width = input_stream_infos[0].shape[1]
                output_buffers[name] = HailoRTTransformUtils._output_raw_buffer_to_nms_with_byte_mask_format(result_array,
                    nms_shape.number_of_classes, batch_size, input_height, input_width,
                    nms_shape.max_bboxes_per_class, output_dtype, self._tf_nms_format)
                continue

            is_nms = output_buffers_info[name].is_nms
            if not is_nms:
                continue
            nms_shape = output_buffers_info[name].vstream_info.nms_shape
            if self._tf_nms_format:
                shape = [batch_size] + output_buffers_info[name].tf_nms_fomrat_shape
                output_dtype = output_buffers_info[name].output_dtype
                quantized_empty_bbox = output_buffers_info[name].quantized_empty_bbox
                flat_result_array = result_array.reshape(-1)
                output_buffers[name] = HailoRTTransformUtils.output_raw_buffer_to_nms_tf_format(flat_result_array, shape,
                    output_dtype, quantized_empty_bbox)
            else:
                output_buffers[name] = HailoRTTransformUtils.output_raw_buffer_to_nms_format(result_array, nms_shape.number_of_classes)

        self._total_time = time.perf_counter() - time_before_infer_calcs
        return output_buffers

    def get_hw_time(self):
        """Get the hardware device operation time it took to run inference over the last batch.

        Returns:
            float: Time in seconds.
        """
        return self._hw_time

    def get_total_time(self):
        """Get the total time it took to run inference over the last batch.

        Returns:
            float: Time in seconds.
        """
        return self._total_time

    def _cast_input_data_if_needed(self, input_layer_name, input_data):
        input_dtype = input_data[input_layer_name].dtype
        with ExceptionWrapper():
            input_expected_dtype = self._infer_pipeline.get_host_dtype(input_layer_name)
        if input_dtype != input_expected_dtype:

            default_logger().warning("Given input data dtype ({}) is different than inferred dtype ({}). "
                "conversion for every frame will reduce performance".format(input_dtype,
                    input_expected_dtype))
            input_data[input_layer_name] = input_data[input_layer_name].astype(input_expected_dtype)
    
    def _validate_input_data_format_type(self, input_layer_name, input_data):
        if input_layer_name not in self._input_vstreams_params:
            return

        input_data_format = self._input_vstreams_params[input_layer_name].user_buffer_format
        input_expected_item_size = _pyhailort.get_format_data_bytes(input_data_format)
        input_item_size = input_data[input_layer_name].dtype.itemsize

        # TODO: Add distinction between float32 and int32 and others
        if input_item_size != input_expected_item_size:
            raise HailoRTException("{} numpy array item size is {}, not {}".format(input_layer_name,
                input_item_size, input_expected_item_size))

    @staticmethod
    def _get_number_of_frames(input_data):
        # Checks that all the batch-sizes of the input_data are equals for all input layers
        if len(input_data) == 0:
            raise ValueError("Input_data can't be empty")
        batch_size_of_first_input = list(input_data.values())[0].shape[0]
        for name, input_data_tensor in input_data.items():
            if input_data_tensor.shape[0] != batch_size_of_first_input:
                raise ValueError(
                    "The number of frames on all input_tensors should be equal! different sizes detected: {} != {}".format(
                        batch_size_of_first_input, input_data_tensor.shape[0]))
        return batch_size_of_first_input

    def _make_c_contiguous_if_needed(self, input_layer_name, input_data):
        if not input_data[input_layer_name].flags.c_contiguous:
            default_logger().warning("Converting {} numpy array to be C_CONTIGUOUS".format(
                input_layer_name))
            input_data[input_layer_name] = numpy.asarray(input_data[input_layer_name], order='C')

    def set_nms_score_threshold(self, threshold):
        """Set NMS score threshold, used for filtering out candidates. Any box with score<TH is suppressed.

        Args:
            threshold (float): NMS score threshold to set.

        Note:
            This function will fail in cases where there is no output with NMS operations on the CPU.
        """
        return self._infer_pipeline.set_nms_score_threshold(threshold)

    def set_nms_iou_threshold(self, threshold):
        """Set NMS intersection over union overlap Threshold,
            used in the NMS iterative elimination process where potential duplicates of detected items are suppressed.

        Args:
            threshold (float): NMS IoU threshold to set.

        Note:
            This function will fail in cases where there is no output with NMS operations on the CPU.
        """
        return self._infer_pipeline.set_nms_iou_threshold(threshold)

    def set_nms_max_proposals_per_class(self, max_proposals_per_class):
        """Set a limit for the maximum number of boxes per class.

        Args:
            max_proposals_per_class (int): NMS max proposals per class to set.

        Note:
            This function will fail in cases where there is no output with NMS operations on the CPU.
        """
        return self._infer_pipeline.set_nms_max_proposals_per_class(max_proposals_per_class)

    def __exit__(self, *args):
        self._infer_pipeline.release()
        return False


class HailoDetection(object):
    """Represents Hailo detection information"""

    def __init__(self, detection):
        self._y_min = detection.box.y_min
        self._x_min = detection.box.x_min
        self._y_max = detection.box.y_max
        self._x_max = detection.box.x_max
        self._score = detection.score
        self._class_id = detection.class_id
        self._mask = detection.mask()

    @property
    def y_min(self):
        """Get detection's box y_min coordinate"""
        return self._y_min

    @property
    def x_min(self):
        """Get detection's box x_min coordinate"""
        return self._x_min

    @property
    def y_max(self):
        """Get detection's box y_max coordinate"""
        return self._y_max

    @property
    def x_max(self):
        """Get detection's box x_max coordinate"""
        return self._x_max

    @property
    def score(self):
        """Get detection's score"""
        return self._score

    @property
    def class_id(self):
        """Get detection's class_id"""
        return self._class_id

    @property
    def mask(self):
        """Byte Mask:
        The mask is a binary mask that defines a region of interest (ROI) of the image.
        Mask pixel values of 1 indicate image pixels that belong to the ROI.
        Mask pixel values of 0 indicate image pixels that are part of the background.

        The size of the mask is the size of the box, in the original input image's dimensions.
        Mask width = ceil((x_max - x_min) * image_width)
        Mask height = ceil((y_max - y_min) * image_height)
        First pixel represents the pixel (x_min * image_width, y_min * image_height) in the original input image.
        """
        return self._mask

class HailoRTTransformUtils(object):
    @staticmethod
    def get_dtype(data_bytes):
        """Get data type from the number of bytes."""
        if data_bytes == 1:
            return numpy.uint8
        elif data_bytes == 2:
            return numpy.uint16
        elif data_bytes == 4:
            return numpy.float32
        raise HailoRTException("unsupported data bytes value")

    @staticmethod
    def dequantize_output_buffer(src_buffer, dst_buffer, elements_count, quant_info):
        """De-quantize the data in input buffer `src_buffer` and output it to the buffer `dst_buffer`

        Args:
            src_buffer (:obj:`numpy.ndarray`): The input buffer containing the data to be de-quantized.
                The buffer's data type is the source data type.
            dst_buffer (:obj:`numpy.ndarray`): The buffer that will contain the de-quantized data.
                The buffer's data type is the destination data type.
            elements_count (int): The number of elements to de-quantize. This number must not exceed 'src_buffer' or 'dst_buffer' sizes.
            quant_info (:class:`~hailo_platform.pyhailort.pyhailort.QuantInfo`): The quantization info.
        """
        with ExceptionWrapper():
            src_format_type = HailoRTTransformUtils._get_format_type(src_buffer.dtype)
            dst_format_type = HailoRTTransformUtils._get_format_type(dst_buffer.dtype)
            if not _pyhailort.is_qp_valid(quant_info):
                raise HailoRTInvalidOperationException("quant_info is invalid as the model was compiled with multiple quant_infos. "
                                                       "Please compile again or provide a list of quant_infos.")
            _pyhailort.dequantize_output_buffer(src_buffer, dst_buffer, src_format_type, dst_format_type, elements_count, quant_info)

    @staticmethod
    def dequantize_output_buffer_in_place(raw_buffer, dst_dtype, elements_count, quant_info):
        """De-quantize the output buffer `raw_buffer` to data type `dst_dtype`.

        Args:
            raw_buffer (:obj:`numpy.ndarray`): The output buffer to be de-quantized. The buffer's data type is the source data type.
            dst_dtype (:obj:`numpy.dtype`): The data type to de-quantize `raw_buffer` to.
            elements_count (int): The number of elements to de-quantize. This number must not exceed 'raw_buffer' size.
            quant_info (:class:`~hailo_platform.pyhailort.pyhailort.QuantInfo`): The quantization info.
        """
        with ExceptionWrapper():
            src_format_type = HailoRTTransformUtils._get_format_type(raw_buffer.dtype)
            dst_format_type = HailoRTTransformUtils._get_format_type(dst_dtype)
            if not _pyhailort.is_qp_valid(quant_info):
                raise HailoRTInvalidOperationException("quant_info is invalid as the model was compiled with multiple quant_infos. "
                                                       "Please compile again or provide a list of quant_infos.")
            _pyhailort.dequantize_output_buffer_in_place(raw_buffer, src_format_type, dst_format_type, elements_count, quant_info)

    @staticmethod
    def quantize_input_buffer(src_buffer, dst_buffer, elements_count, quant_info):
        """Quantize the data in input buffer `src_buffer` and output it to the buffer `dst_buffer`

        Args:
            src_buffer (:obj:`numpy.ndarray`): The input buffer containing the data to be quantized.
                The buffer's data type is the source data type.
            dst_buffer (:obj:`numpy.ndarray`): The buffer that will contain the quantized data.
                The buffer's data type is the destination data type.
            elements_count (int): The number of elements to quantize. This number must not exceed 'src_buffer' or 'dst_buffer' sizes.
            quant_info (:class:`~hailo_platform.pyhailort.pyhailort.QuantInfo`): The quantization info.
        """
        with ExceptionWrapper():
            src_format_type = HailoRTTransformUtils._get_format_type(src_buffer.dtype)
            dst_format_type = HailoRTTransformUtils._get_format_type(dst_buffer.dtype)
            if not _pyhailort.is_qp_valid(quant_info):
                raise HailoRTInvalidOperationException("quant_info is invalid as the model was compiled with multiple quant_infos. "
                                                       "Please compile again or provide a list of quant_infos.")
            _pyhailort.quantize_input_buffer(src_buffer, dst_buffer, src_format_type, dst_format_type, elements_count, quant_info)

    @staticmethod
    def output_raw_buffer_to_nms_tf_format(raw_output_buffer, shape, dtype, quantized_empty_bbox):
        offset = 0
        # We create the tf_format buffer with reversed width/features for preformance optimization
        converted_output_buffer = numpy.empty([shape[0], shape[1], shape[3], shape[2]], dtype=dtype)
        for frame in range(converted_output_buffer.shape[0]):
            offset = frame * converted_output_buffer.shape[1] * (converted_output_buffer.shape[2] * converted_output_buffer.shape[3] + 1)
            HailoRTTransformUtils.output_raw_buffer_to_nms_tf_format_single_frame(raw_output_buffer, converted_output_buffer[frame],
                converted_output_buffer.shape[1], converted_output_buffer.shape[2], quantized_empty_bbox, offset)
        converted_output_buffer = numpy.swapaxes(converted_output_buffer, 2, 3)
        return converted_output_buffer

    @staticmethod
    def output_raw_buffer_to_nms_tf_format_single_frame(raw_output_buffer, converted_output_frame, number_of_classes,
        max_bboxes_per_class, quantized_empty_bbox, offset=0):
        for class_i in range(number_of_classes):
            class_bboxes_amount = int(raw_output_buffer[offset])
            offset += 1
            if 0 != class_bboxes_amount:
                converted_output_frame[class_i][ : class_bboxes_amount][:] = raw_output_buffer[offset : offset + (BBOX_PARAMS * class_bboxes_amount)].reshape(class_bboxes_amount, BBOX_PARAMS)
                offset += BBOX_PARAMS * class_bboxes_amount
            converted_output_frame[class_i][class_bboxes_amount : max_bboxes_per_class][:] = quantized_empty_bbox

    @staticmethod
    def output_raw_buffer_to_nms_format(raw_output_buffer, number_of_classes):
        converted_output_buffer = []
        for frame in raw_output_buffer:
            converted_output_buffer.append(HailoRTTransformUtils.output_raw_buffer_to_nms_format_single_frame(frame, number_of_classes))
        return converted_output_buffer

    @staticmethod
    def output_raw_buffer_to_nms_format_single_frame(raw_output_buffer, number_of_classes, offset=0):
        converted_output_frame = []
        for class_i in range(number_of_classes):
            class_bboxes_amount = int(raw_output_buffer[offset])
            offset += 1
            if class_bboxes_amount == 0:
                converted_output_frame.append(numpy.empty([0, BBOX_PARAMS]))
            else:
                converted_output_frame.append(raw_output_buffer[offset : offset + (BBOX_PARAMS * class_bboxes_amount)].reshape(
                    class_bboxes_amount, BBOX_PARAMS))
                offset += BBOX_PARAMS * class_bboxes_amount
        return converted_output_frame

    @staticmethod
    def _output_raw_buffer_to_nms_with_byte_mask_format(raw_output_buffer, number_of_classes, batch_size, image_height, image_width,
            max_bboxes_per_class, output_dtype, is_tf_format=False):
        if is_tf_format:
            return HailoRTTransformUtils._output_raw_buffer_to_nms_with_byte_mask_tf_format(raw_output_buffer, number_of_classes,
                batch_size, image_height, image_width, max_bboxes_per_class, output_dtype)
        else:
            return HailoRTTransformUtils._output_raw_buffer_to_nms_with_byte_mask_hailo_format(raw_output_buffer)

    @staticmethod
    def _output_raw_buffer_to_nms_with_byte_mask_hailo_format(raw_output_buffer):
        converted_output_buffer = []
        for frame in raw_output_buffer:
            converted_output_buffer.append(
                HailoRTTransformUtils._output_raw_buffer_to_nms_with_byte_mask_hailo_format_single_frame(frame))
        return converted_output_buffer

    @staticmethod
    def _output_raw_buffer_to_nms_with_byte_mask_hailo_format_single_frame(raw_output_buffer):
        detections = _pyhailort.convert_nms_with_byte_mask_buffer_to_detections(raw_output_buffer)
        converted_output_frame = []
        for detection in detections:
            converted_output_frame.append(HailoDetection(detection))

        return converted_output_frame

    @staticmethod
    def _output_raw_buffer_to_nms_with_byte_mask_tf_format(raw_output_buffer, number_of_classes, batch_size, image_height, image_width,
            max_bboxes_per_class, output_dtype):

        BBOX_WITH_MASK_PARAMS = 6 # 4 coordinates + score + class_idx
        BBOX_WITH_MASK_AXIS = 2
        CLASSES_AXIS = 1

        # We create the tf_format buffer with reversed max_bboxes_per_class/features for performance optimization
        converted_output_buffer = numpy.empty([batch_size, max_bboxes_per_class, (image_height * image_width + BBOX_WITH_MASK_PARAMS)], dtype=output_dtype)

        for frame_idx in range(len(raw_output_buffer)):
            HailoRTTransformUtils._output_raw_buffer_to_nms_with_byte_mask_tf_format_single_frame(
                raw_output_buffer[frame_idx], converted_output_buffer[frame_idx], number_of_classes, max_bboxes_per_class,
                image_height, image_width)
        converted_output_buffer = numpy.moveaxis(converted_output_buffer, CLASSES_AXIS, BBOX_WITH_MASK_AXIS)
        converted_output_buffer = numpy.expand_dims(converted_output_buffer, 1)
        return converted_output_buffer

    @staticmethod
    def _output_raw_buffer_to_nms_with_byte_mask_tf_format_single_frame(raw_output_buffer, converted_output_frame, number_of_classes,
        max_boxes, image_height, image_width):

        detections = _pyhailort.convert_nms_with_byte_mask_buffer_to_detections(raw_output_buffer)
        bbox_idx = 0
        for detection in detections:
            if (bbox_idx >= max_boxes):
                return
            bbox = numpy.array([detection.box.y_min, detection.box.x_min, detection.box.y_max, detection.box.x_max,
                    detection.score, detection.class_id])
            bbox_mask = detection.mask()

            y_min = numpy.ceil(bbox[0] * image_height)
            x_min = numpy.ceil(bbox[1] * image_width)
            bbox_width = numpy.ceil((bbox[3] - bbox[1]) * image_width)
            resized_mask = numpy.zeros(image_height*image_width, dtype="uint8")

            for i in range(bbox_mask.size):
                if (bbox_mask[i] == 1):
                    x = int(x_min + (i % bbox_width))
                    y = int(y_min + (i // bbox_width))
                    if (x >= image_width):
                        x = image_width - 1
                    if ( y >= image_height):
                        y = image_height - 1
                    idx = (image_width * y) + x
                    resized_mask[idx] = 1

            bbox_with_mask = numpy.append(bbox, resized_mask)
            converted_output_frame[bbox_idx] = bbox_with_mask
            bbox_idx += 1


    @staticmethod
    def _get_format_type(dtype):
        if dtype == numpy.uint8:
            return FormatType.UINT8
        elif dtype == numpy.uint16:
            return FormatType.UINT16
        elif dtype == numpy.float32:
            return FormatType.FLOAT32
        raise HailoRTException("unsupported data type {}".format(dtype))

# TODO: HRT-10427 - Remove
class InternalEthernetDevice(object):
    def __init__(self, address, port, response_timeout_seconds=10, max_number_of_attempts=3):
        self.device = None
        self._address = address
        self._port = port
        self._response_timeout_milliseconds = int(response_timeout_seconds * 1000)
        self._max_number_of_attempts = max_number_of_attempts
        with ExceptionWrapper():
            self.device = _pyhailort.Device.create_eth(self._address, self._port,
                self._response_timeout_milliseconds, self._max_number_of_attempts)

    def __del__(self):
        self.release()

    def release(self):
        if self.device is None:
            return
        with ExceptionWrapper():
            self.device.release()
            self.device = None


class PcieDeviceInfo(_pyhailort.PcieDeviceInfo):
    """Represents pcie device info, includeing domain, bus, device and function.
    """

    BOARD_LOCATION_HELP_STRING = 'Board location in the format of the command: "lspci -d 1e60: | cut -d\' \' -f1" ([<domain>]:<bus>:<device>.<func>). If not specified the first board is taken.'

    def __init__(self, bus, device, func, domain=None):
        super(PcieDeviceInfo, self).__init__()
        self.bus = bus
        self.device = device
        self.func = func
        if domain is None:
            self.domain = PCIE_ANY_DOMAIN
        else:
            self.domain = domain

    def __eq__(self, other):
        return (self.domain, self.bus, self.device, self.func) == (other.domain, other.bus, other.device, other.func)

    def __str__(self):
        with ExceptionWrapper():
            return super().__str__()

    def __repr__(self):
        return 'PcieDeviceInfo({})'.format(str(self))

    @classmethod
    def from_string(cls, board_location_str):
        """Parse pcie device info BDF from string. The format is [<domain>]:<bus>:<device>.<func>"""
        with ExceptionWrapper():
            device_info = _pyhailort.PcieDeviceInfo._parse(board_location_str)
            return PcieDeviceInfo(device_info.bus, device_info.device, device_info.func, device_info.domain)

    @classmethod
    def argument_type(cls, board_location_str):
        """PcieDeviceInfo Argument type for argparse parsers"""
        try:
            return cls.from_string(board_location_str)
        except HailoRTException:
            raise ArgumentTypeError('Invalid device info string, format is [<domain>]:<bus>:<device>.<func>')

# TODO: HRT-10427 - Remove
class InternalPcieDevice(object):
    def __init__(self, device_info=None):
        self.device = None
        if device_info is None:
            device_info = InternalPcieDevice.scan_devices()[0]
        self._device_info = device_info
        with ExceptionWrapper():
            self.device = _pyhailort.Device.create_pcie(self._device_info)

    def __del__(self):
        self.release()

    def release(self):
        if self.device is None:
            return
        with ExceptionWrapper():
            self.device.release()
            self.device = None

    # TODO: HRT-10427 - Move to a static method in pyhailort_internal when InternalPcieDevice removed
    @staticmethod
    def scan_devices():
        with ExceptionWrapper():
            pcie_scanner = _pyhailort.PcieScan()
            return [PcieDeviceInfo(dev_info.bus, dev_info.device, dev_info.func, dev_info.domain)
                for dev_info in pcie_scanner.scan_devices()]

    def create_debug_log(self):
        return PcieDebugLog(self)

    def write_memory(self, address, data):
        with ExceptionWrapper():
            self.device.direct_write_memory(address, data)

    def read_memory(self, address, size):
        with ExceptionWrapper():
            return self.device.direct_read_memory(address, size)

# TODO: HRT-10427 - Remove when removing InternalPcieDevice
class PcieDebugLog(object):
    def __init__(self, pci_device):
        self._pcie_device = pci_device

    def read(self, count, cpu_id):
        with ExceptionWrapper():
            return self._pcie_device.device.read_log(count, cpu_id)


class HailoPowerMeasurementUtils(object):
    @staticmethod
    def return_real_sampling_period(sampling_period):
        """Get a sampling period from the enum."""
        SamplingPeriodDictionary = dict([
                (SamplingPeriod.PERIOD_140us, 140),
                (SamplingPeriod.PERIOD_204us, 204),
                (SamplingPeriod.PERIOD_332us, 332),
                (SamplingPeriod.PERIOD_588us, 588),
                (SamplingPeriod.PERIOD_1100us, 1100),
                (SamplingPeriod.PERIOD_2116us, 2116),
                (SamplingPeriod.PERIOD_4156us, 4156),
                (SamplingPeriod.PERIOD_8244us, 8244),
        ])
        return SamplingPeriodDictionary[sampling_period]

    @staticmethod
    def return_real_averaging_factor(averaging_factor):
        """Get an averaging factor from the enum."""
        AveragingFactorDictionary = dict([
                (AveragingFactor.AVERAGE_1, 1),
                (AveragingFactor.AVERAGE_4, 4),
                (AveragingFactor.AVERAGE_16, 16),
                (AveragingFactor.AVERAGE_64, 64),
                (AveragingFactor.AVERAGE_128, 128),
                (AveragingFactor.AVERAGE_256, 256),
                (AveragingFactor.AVERAGE_512, 512),
                (AveragingFactor.AVERAGE_1024, 1024),
        ])
        return AveragingFactorDictionary[averaging_factor]

class HailoPowerMode(_pyhailort.PowerMode):
    pass

class HailoStreamInterface(_pyhailort.StreamInterface):
    pass

class HailoStreamDirection(_pyhailort.StreamDirection):
    pass

class HailoCpuId(_pyhailort.CpuId):
    pass

class HailoFormatFlags(_pyhailort.FormatFlags):
    pass

SUPPORTED_PROTOCOL_VERSION = 2
SUPPORTED_FW_MAJOR = 4
SUPPORTED_FW_MINOR = 16
SUPPORTED_FW_REVISION = 2

MEGA_MULTIPLIER = 1000.0 * 1000.0


class DeviceArchitectureTypes(IntEnum):
    HAILO8_A0 = 0
    HAILO8 = 1
    HAILO8L = 2
    HAILO15H = 3
    PLUTO = 4

    def __str__(self):
        return self.name

class BoardInformation(object):
    def __init__(self, protocol_version, fw_version_major, fw_version_minor, fw_version_revision,
                 logger_version, board_name, is_release, extended_context_switch_buffer, device_architecture,
                 serial_number, part_number, product_name):
        self.protocol_version = protocol_version
        self.firmware_version = HailoFirmwareVersion.construct_from_params(fw_version_major, fw_version_minor, fw_version_revision, is_release,
            extended_context_switch_buffer, HailoFirmwareType.APP)
        self.logger_version = logger_version
        self.board_name = board_name
        self.is_release = is_release
        self.device_architecture = DeviceArchitectureTypes(device_architecture)
        self.serial_number = serial_number
        self.part_number = part_number
        self.product_name = product_name
    
    def _string_field_str(self, string_field):
        # Return <N/A> if the string field is empty
        return string_field.rstrip('\x00') or "<N/A>"

    def __str__(self):
        """Returns:
            str: Human readable string.
        """
        return 'Control Protocol Version: {}\n' \
               'Firmware Version: {}\n' \
               'Logger Version: {}\n' \
               'Board Name: {}\n' \
               'Device Architecture: {}\n' \
               'Serial Number: {}\n' \
               'Part Number: {}\n' \
               'Product Name: {}\n'.format(
            self.protocol_version,
            self.firmware_version,
            self.logger_version,
            self.board_name.rstrip('\x00'),
            str(self.device_architecture),
            self._string_field_str(self.serial_number),
            self._string_field_str(self.part_number),
            self._string_field_str(self.product_name))
       
    def __repr__(self):
        """Returns:
            str: Human readable string.
        """ 
        return self.__str__()

    @staticmethod
    def get_hw_arch_str(device_arch):
        if ((device_arch == DeviceArchitectureTypes.HAILO8) or
            (device_arch == DeviceArchitectureTypes.HAILO8L)):
            return 'hailo8'
        elif device_arch == DeviceArchitectureTypes.HAILO15H:
            return 'hailo15'
        else:
            raise HailoRTException("Unsupported device architecture.")

class CoreInformation(object):
    def __init__(self, fw_version_major, fw_version_minor, fw_version_revision, is_release, extended_context_switch_buffer):
        self.firmware_version = HailoFirmwareVersion.construct_from_params(fw_version_major, fw_version_minor, fw_version_revision, is_release,
            extended_context_switch_buffer, HailoFirmwareType.CORE)
        self.is_release = is_release
    
    def __str__(self):
        """Returns:
            str: Human readable string.
        """
        return 'Core Firmware Version: {}'.format(
            self.firmware_version)

    def __repr__(self):
        """Returns:
            str: Human readable string.
        """
        return self.__str__()

class TemperatureThrottlingLevel(object):
    def __init__(self, level_number, temperature_threshold, hysteresis_temperature_threshold, throttling_nn_clock_freq):
        self.level_number = level_number
        self.temperature_threshold = temperature_threshold
        self.hysteresis_temperature_threshold = hysteresis_temperature_threshold
        self.throttling_nn_clock_freq = throttling_nn_clock_freq

    def __str__(self):
        """Returns:
            str: Human readable string.
        """
        return 'Temperature Throttling Level {}: \n' \
               'Temperature Threshold: {}\n' \
               'Hysteresis Temperature Threshold: {}\n' \
               'Throttling NN Clock Frequency: {}\n' \
               .format(self.level_number, self.temperature_threshold, self.hysteresis_temperature_threshold, self.throttling_nn_clock_freq)
        
    def __repr__(self):
        return self.__str__()

class HealthInformation(object):
    def __init__(self, overcurrent_protection_active, current_overcurrent_zone, red_overcurrent_threshold, overcurrent_throttling_active,
                       temperature_throttling_active, current_temperature_zone, current_temperature_throttling_level,
                       temperature_throttling_levels, orange_temperature_threshold, orange_hysteresis_temperature_threshold, 
                       red_temperature_threshold, red_hysteresis_temperature_threshold, requested_overcurrent_clock_freq, requested_temperature_clock_freq):
        self.overcurrent_protection_active = overcurrent_protection_active
        self.current_overcurrent_zone = current_overcurrent_zone
        self.red_overcurrent_threshold = red_overcurrent_threshold
        self.overcurrent_throttling_active = overcurrent_throttling_active
        self.temperature_throttling_active = temperature_throttling_active
        self.current_temperature_zone = current_temperature_zone
        self.current_temperature_throttling_level = current_temperature_throttling_level
        self.orange_temperature_threshold = orange_temperature_threshold
        self.orange_hysteresis_temperature_threshold = orange_hysteresis_temperature_threshold
        self.red_temperature_threshold = red_temperature_threshold
        self.red_hysteresis_temperature_threshold = red_hysteresis_temperature_threshold
        self.requested_overcurrent_clock_freq = requested_overcurrent_clock_freq
        self.requested_temperature_clock_freq = requested_temperature_clock_freq
        
        # Add TemperatureThrottlingLevel in case it has new throttling_nn_clock_freq. level_number can be used as only last
        # levels can be with the same freq
        self.temperature_throttling_levels = []
        if self.temperature_throttling_active:
            throttling_nn_clock_frequencies = []
            for level_number, temperature_throttling_level in enumerate(temperature_throttling_levels):
                if temperature_throttling_level.throttling_nn_clock_freq not in throttling_nn_clock_frequencies:
                    throttling_nn_clock_frequencies.append(temperature_throttling_level.throttling_nn_clock_freq)
                    self.temperature_throttling_levels.append(TemperatureThrottlingLevel(level_number,
                                                                                        temperature_throttling_level.temperature_threshold, 
                                                                                        temperature_throttling_level.hysteresis_temperature_threshold, 
                                                                                        temperature_throttling_level.throttling_nn_clock_freq))
    def __repr__(self):
        return self.__str__()

    def __str__(self):
        """Returns:
            str: Human readable string.
        """
        temperature_throttling_levels_str = "\n".join(["\n\n{}\n".format(str(temperature_throttling_level)) for temperature_throttling_level in self.temperature_throttling_levels]) \
                                            if self.temperature_throttling_active else "<Temperature throttling is disabled>"
        return 'Overcurrent Protection Active: {}\n' \
               'Overcurrent Protection Current Overcurrent Zone: {}\n' \
               'Overcurrent Protection Red Threshold: {}\n' \
               'Overcurrent Protection Throttling State: {}\n' \
               'Temperature Protection Red Threshold: {}\n' \
               'Temperature Protection Red Hysteresis Threshold: {}\n' \
               'Temperature Protection Orange Threshold: {}\n' \
               'Temperature Protection Orange Hysteresis Threshold: {}\n' \
               'Temperature Protection Throttling State: {}\n' \
               'Temperature Protection Current Zone: {}\n' \
               'Temperature Protection Current Throttling Level: {}\n' \
               'Temperature Protection Throttling Levels: {}' \
               .format(self.overcurrent_protection_active, self.current_overcurrent_zone, self.red_overcurrent_threshold, 
                       self.overcurrent_throttling_active, self.red_temperature_threshold, 
                       self.red_hysteresis_temperature_threshold, self.orange_temperature_threshold, 
                       self.orange_hysteresis_temperature_threshold, self.temperature_throttling_active,
                       self.current_temperature_zone, self.current_temperature_throttling_level, temperature_throttling_levels_str)

class ExtendedDeviceInformation(object):
    def __init__(self, neural_network_core_clock_rate, supported_features, boot_source, lcs, soc_id, eth_mac_address, unit_level_tracking_id, soc_pm_values):
        self.neural_network_core_clock_rate = neural_network_core_clock_rate
        self.supported_features = SupportedFeatures(supported_features)
        self.boot_source = boot_source
        self.lcs = lcs
        self.soc_id = soc_id
        self.eth_mac_address = eth_mac_address
        self.unit_level_tracking_id = unit_level_tracking_id
        self.soc_pm_values = soc_pm_values

    def __str__(self):
        """Returns:
            str: Human readable string.
        """
        string = 'Neural Network Core Clock Rate: {}MHz\n' \
                 '{}' \
                 'Boot source: {}\n' \
                 'LCS: {}\n'.format(
            self.neural_network_core_clock_rate / MEGA_MULTIPLIER,
            str(self.supported_features),
            str(self.boot_source.name),
            str(self.lcs))
        if any(self.soc_id):
            string += 'SoC ID: ' + (self.soc_id.hex())

        if any(self.eth_mac_address):
            string += '\nMAC Address: ' + (":".join("{:02X}".format(i) for i in self.eth_mac_address))
        
        if any(self.unit_level_tracking_id):
            string += '\nULT ID: ' + (self.unit_level_tracking_id.hex())
        
        if any(self.soc_pm_values):
            string += '\nPM Values: ' + (self.soc_pm_values.hex())


        return string

    def __repr__(self):
        """Returns:
            str: Human readable string.
        """
        return self.__str__()

class HailoFirmwareMode(Enum):
    """Indication that firmware version is stable and official  """
    DEVELOP = 'develop'
    RELEASE = 'release'


class HailoFirmwareType(Enum):
    """Indication the firmware type """
    CORE = 'core'
    APP = 'app'


class HailoFirmwareVersion(object):
    """Represents a Hailo chip firmware version."""
    CORE_BIT                           = 0x08000000
    EXTENDED_CONTEXT_SWITCH_BUFFER_BIT = 0x40000000
    DEV_BIT                            = 0x80000000
    FW_VERSION_FORMAT = '<III'

    def __init__(self, firmware_version_buffer, is_release, extended_context_switch_buffer, fw_type):
        """Initialize a new Hailo Firmware Version object.

        Args:
            firmware_version_buffer (str): A buffer containing the firmware version struct.
            is_release (bool, optional): Flag indicating if firmware is at develop/release mode.
                                         None indicates unknown
            extended_context_switch_buffer (bool): Flag indicating if firmware has an extended context switch buffer.
            fw_type (HailoFirmwareType): Firmware type
        """
        self.major, self.minor, self.revision = struct.unpack(
            self.FW_VERSION_FORMAT,
            firmware_version_buffer)
        
        self.fw_type = fw_type
        self.mode = HailoFirmwareMode.RELEASE if is_release else HailoFirmwareMode.DEVELOP
        self.extended_context_switch_buffer = extended_context_switch_buffer
        
        self.revision &= ~(self.CORE_BIT | self.EXTENDED_CONTEXT_SWITCH_BUFFER_BIT | self.DEV_BIT)

    def __str__(self):
        """Returns:
            str: Firmware version in a human readable format.
        """
        return '{}.{}.{} ({},{}{})'.format(self.major, self.minor, self.revision, self.mode.value, self.fw_type.value,
            ',extended context switch buffer' if self.extended_context_switch_buffer else '')

    @classmethod
    def construct_from_params(cls, major, minor, revision, is_release, extended_context_switch_buffer, fw_type):
        """Returns:
            class HailoFirmwareVersion : with the given Firmware version.
        """
        return cls(struct.pack(HailoFirmwareVersion.FW_VERSION_FORMAT, major, minor, revision), is_release,
            extended_context_switch_buffer, fw_type)

    @property
    def comparable_value(self):
        """A value that could be compared to other firmware versions."""
        return (self.major << 64) + (self.minor << 32) + (self.revision)

    def __hash__(self):
        return self.comparable_value

    def __eq__(self, other):
        return self.comparable_value == other.comparable_value

    def __lt__(self, other):
        return self.comparable_value < other.comparable_value

    def check_protocol_compatibility(self, other):
        return ((self.major == other.major) and (self.minor == other.minor))

class SupportedFeatures(object):
    def __init__(self, supported_features):
        self.ethernet = supported_features.ethernet
        self.mipi = supported_features.mipi
        self.pcie = supported_features.pcie
        self.current_monitoring = supported_features.current_monitoring
        self.mdio = supported_features.mdio
    
    def _feature_str(self, feature_name, is_feature_enabled):
        return '{}: {}\n'.format(feature_name, 'Enabled' if is_feature_enabled else 'Disabled')

    def __str__(self):
        """Returns:
            str: Human readable string.
        """
        return 'Device supported features: \n' + \
            self._feature_str('Ethernet', self.ethernet) + \
            self._feature_str('MIPI', self.mipi) + \
            self._feature_str('PCIE', self.pcie) + \
            self._feature_str('Current Monitoring', self.current_monitoring) + \
            self._feature_str('MDIO', self.mdio)

    def __repr__(self):
        """Returns:
            str: Human readable string.
        """
        return self.__str__()

    def _is_feature_enabled(self, feature):
        return (self.supported_features & feature) != 0

class Control:
    """The control object of this device, which implements the control API of the Hailo device.
    Should be used only from Device.control"""

    WORD_SIZE = 4

    def __init__(self, device: '_pyhailort.Device'):
        self.__device = device

        # TODO: should remove?
        if sys.platform != "win32":
            signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGWINCH])

        self._identify_info = self.identify()

    @property
    def _device(self):
        if not self.__device.is_valid():
            raise HailoRTInvalidOperationException("The device in use has been released. "
                "This can happen if 'device.release()' has been called, or one-liner usage of control 'Device().control.XX()'")
        return self.__device

    @property
    def device_id(self):
        """Getter for the device_id.

        Returns:
            str: A string ID of the device. BDF for PCIe devices, IP address for Ethernet devices, "Integrated" for integrated nnc devices.
        """
        return self._device.device_id

    def open(self):
        """Initializes the resources needed for using a control device."""
        pass

    def close(self):
        """Releases the resources that were allocated for the control device."""
        pass

    def chip_reset(self):
        """Resets the device (chip reset)."""
        with ExceptionWrapper():
            return self._device.reset(_pyhailort.ResetDeviceMode.CHIP)

    def nn_core_reset(self):
        """Resets the nn_core."""
        with ExceptionWrapper():
            return self._device.reset(_pyhailort.ResetDeviceMode.NN_CORE)

    def soft_reset(self):
        """reloads the device firmware (soft reset)"""
        with ExceptionWrapper():
            return self._device.reset(_pyhailort.ResetDeviceMode.SOFT)

    def forced_soft_reset(self):
        """reloads the device firmware (forced soft reset)"""
        with ExceptionWrapper():
            return self._device.reset(_pyhailort.ResetDeviceMode.FORCED_SOFT)

    def read_memory(self, address, data_length):
        """Reads memory from the Hailo chip. Byte order isn't changed. The core uses little-endian
        byte order.

        Args:
            address (int): Physical address to read from.
            data_length (int): Size to read in bytes.

        Returns:
            list of str: Memory read from the chip, each index in the list is a byte
        """
        with ExceptionWrapper():
            return self._device.read_memory(address, data_length)

    def write_memory(self, address, data_buffer):
        """Write memory to Hailo chip. Byte order isn't changed. The core uses little-endian byte
        order.

        Args:
            address (int): Physical address to write to.
            data_buffer (list of str): Data to write.
        """
        with ExceptionWrapper():
            return self._device.write_memory(address, data_buffer, len(data_buffer))

    def configure(self, hef, configure_params_by_name={}):
        """
        Configures device from HEF object.

        Args:
            hef (:class:`~hailo_platform.pyhailort.pyhailort.HEF`): HEF to configure the
                device from.
            configure_params_by_name (dict, optional): Maps between each net_group_name to
                configure_params. In case of a mismatch with net_groups_names, default params will
                be used.
        """
        with ExceptionWrapper():
            return self._device.configure(hef._hef, configure_params_by_name)

    def power_measurement(self, dvm=DvmTypes.AUTO, measurement_type=PowerMeasurementTypes.AUTO):
        """Perform a single power measurement on an Hailo chip. It works with the default settings
        where the sensor returns a new value every 2.2 ms without averaging the values.

        Args:
            dvm (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes`):
                Which DVM will be measured. Default (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AUTO`) will be different according to the board: \n
                 Default (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AUTO`) for EVB is an approximation to the total power consumption of the chip in PCIe setups.
                 It sums :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.VDD_CORE`,
                 :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.MIPI_AVDD` and :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AVDD_H`.
                 Only :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.POWER` can measured with this option. \n
                 Default (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AUTO`) for platforms supporting current monitoring (such as M.2 and mPCIe): :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.OVERCURRENT_PROTECTION`
            measurement_type
             (:class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`):
             The type of the measurement.

        Returns:
            float: The measured power. \n
            For :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`: \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.SHUNT_VOLTAGE`: Unit is mV. \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.BUS_VOLTAGE`: Unit is mV. \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.POWER`: Unit is W. \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.CURRENT`: Unit is mA. \n


        Note:
            This function can perform measurements for more than just power. For all supported
            measurement types, please look at
            :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`.
        """

        with ExceptionWrapper():
            return self._device.power_measurement(dvm, measurement_type)

    def start_power_measurement(self, averaging_factor=AveragingFactor.AVERAGE_256, sampling_period=SamplingPeriod.PERIOD_1100us):
        """Start performing a long power measurement.

        Args:
            averaging_factor (:class:`~hailo_platform.pyhailort.pyhailort.AveragingFactor`):
                Number of samples per time period, sensor configuration value.
            sampling_period (:class:`~hailo_platform.pyhailort.pyhailort.SamplingPeriod`):
                Related conversion time, sensor configuration value. The sensor samples the power
                every ``sampling_period`` [ms] and averages every ``averaging_factor`` samples. The
                sensor provides a new value every: 2 * sampling_period * averaging_factor [ms]. The
                firmware wakes up every ``delay`` [ms] and checks the sensor. If there is a new`
                value to read from the sensor, the firmware reads it.  Note that the average
                calculated by the firmware is "average of averages", because it averages values
                that have already been averaged by the sensor.
        """
        with ExceptionWrapper():
            return self._device.start_power_measurement(averaging_factor, sampling_period)

    def stop_power_measurement(self):
        """Stop performing a long power measurement. Deletes all saved results from the firmware.
        Calling the function eliminates the start function settings for the averaging the samples,
        and returns to the default values, so the sensor will return a new value every 2.2 ms
        without averaging values.
        """
        with ExceptionWrapper():
            return self._device.stop_power_measurement()

    def set_power_measurement(self, buffer_index=MeasurementBufferIndex.MEASUREMENT_BUFFER_INDEX_0, dvm=DvmTypes.AUTO, measurement_type=PowerMeasurementTypes.AUTO):
        """Set parameters for long power measurement on an Hailo chip.

        Args:
            buffer_index (:class:`~hailo_platform.pyhailort.pyhailort.MeasurementBufferIndex`): Index of the buffer on the firmware the data would be saved at.
                Default is :class:`~hailo_platform.pyhailort.pyhailort.MeasurementBufferIndex.MEASUREMENT_BUFFER_INDEX_0`
            dvm (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes`):
                Which DVM will be measured. Default (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AUTO`) will be different according to the board: \n
                 Default (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AUTO`) for EVB is an approximation to the total power consumption of the chip in PCIe setups.
                 It sums :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.VDD_CORE`,
                 :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.MIPI_AVDD` and :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AVDD_H`.
                 Only :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.POWER` can measured with this option. \n
                 Default (:class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.AUTO`) for platforms supporting current monitoring (such as M.2 and mPCIe): :class:`~hailo_platform.pyhailort.pyhailort.DvmTypes.OVERCURRENT_PROTECTION`
            measurement_type
             (:class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`):
             The type of the measurement.

        Note:
            This function can perform measurements for more than just power. For all supported measurement types
            view :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`
        """
        with ExceptionWrapper():
            return self._device.set_power_measurement(buffer_index, dvm, measurement_type)

    def get_power_measurement(self, buffer_index=MeasurementBufferIndex.MEASUREMENT_BUFFER_INDEX_0, should_clear=True):
        """Read measured power from a long power measurement

        Args:
            buffer_index (:class:`~hailo_platform.pyhailort.pyhailort.MeasurementBufferIndex`): Index of the buffer on the firmware the data would be saved at.
                Default is :class:`~hailo_platform.pyhailort.pyhailort.MeasurementBufferIndex.MEASUREMENT_BUFFER_INDEX_0`
            should_clear (bool): Flag indicating if the results saved at the firmware will be deleted after reading.

        Returns:
            :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementData`:
             Object containing measurement data \n
            For :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`: \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.SHUNT_VOLTAGE`: Unit is mV. \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.BUS_VOLTAGE`: Unit is mV. \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.POWER`: Unit is W. \n
            - :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes.CURRENT`: Unit is mA. \n

        Note:
            This function can perform measurements for more than just power.
            For all supported measurement types view
            :class:`~hailo_platform.pyhailort.pyhailort.PowerMeasurementTypes`.
        """

        with ExceptionWrapper():
            return self._device.get_power_measurement(buffer_index, should_clear)

    def _examine_user_config(self):
        with ExceptionWrapper():
            return self._device.examine_user_config()

    def read_user_config(self):
        """Read the user configuration section as binary data.

        Returns:
            str: User config as a binary buffer.
        """
        with ExceptionWrapper():
            return self._device.read_user_config()

    def write_user_config(self, configuration):
        """Write the user configuration.

        Args:
            configuration (str): A binary representation of a Hailo device configuration.
        """
        with ExceptionWrapper():
            return self._device.write_user_config(configuration)

    def _erase_user_config(self):
        with ExceptionWrapper():
            return self._device.erase_user_config()

    def read_board_config(self):
        """Read the board configuration section as binary data.

        Returns:
            str: Board config as a binary buffer.
        """
        with ExceptionWrapper():
            return self._device.read_board_config()

    def write_board_config(self, configuration):
        """Write the static configuration.

        Args:
            configuration (str): A binary representation of a Hailo device configuration.
        """
        with ExceptionWrapper():
            return self._device.write_board_config(configuration)

    def identify(self):
        """Gets the Hailo chip identification.

        Returns:
            :class:`~hailo_platform.pyhailort.pyhailort.BoardInformation`
        """
        with ExceptionWrapper():
            response = self._device.identify()
        board_information = BoardInformation(response.protocol_version, response.fw_version.major,
            response.fw_version.minor, response.fw_version.revision, response.logger_version,
            response.board_name, response.is_release, response.extended_context_switch_buffer,
            int(response.device_architecture), response.serial_number, response.part_number,
            response.product_name)
        return board_information

    def core_identify(self):
        """Gets the Core Hailo chip identification.

        Returns:
            :class:`~hailo_platform.pyhailort.pyhailort.CoreInformation`
        """
        with ExceptionWrapper():
            response =  self._device.core_identify()
        core_information = CoreInformation(response.fw_version.major, response.fw_version.minor, 
            response.fw_version.revision, response.is_release, response.extended_context_switch_buffer)
        return core_information

    def set_fw_logger(self, level, interface_mask):
        """Configure logger level and interface of sending.

        Args:
            level (FwLoggerLevel):    The minimum logger level.
            interface_mask (int):     Output interfaces (mix of FwLoggerInterface).
        """
        with ExceptionWrapper():
            return self._device.set_fw_logger(level, interface_mask)

    def set_throttling_state(self, should_activate):
        """Change throttling state of temperature protection and overcurrent protection components.
           In case that change throttling state of temperature protection didn't succeed,
           the change throttling state of overcurrent protection is executed.

        Args:
            should_activate (bool):   Should be true to enable or false to disable. 
        """
        with ExceptionWrapper():
            return self._device.set_throttling_state(should_activate)

    def get_throttling_state(self):
        """Get the current throttling state of temperature protection and overcurrent protection components.
           If any throttling is enabled, the function return true.

        Returns:
            bool: true if temperature or overcurrent protection throttling is enabled, false otherwise.
        """
        with ExceptionWrapper():
            return self._device.get_throttling_state()

    def _set_overcurrent_state(self, should_activate):
        """Control whether the overcurrent protection is enabled or disabled.

        Args:
            should_activate (bool):   Should be true to enable or false to disable. 
        """
        with ExceptionWrapper():
            return self._device._set_overcurrent_state(should_activate)

    def _get_overcurrent_state(self):
        """Get the overcurrent protection state.
        
        Returns:
            bool: true if overcurrent protection is enabled, false otherwise.
        """
        with ExceptionWrapper():
            return self._device._get_overcurrent_state()

    @staticmethod
    def _create_c_i2c_slave(pythonic_slave):
        c_slave = _pyhailort.I2CSlaveConfig()
        c_slave.endianness = pythonic_slave.endianness
        c_slave.slave_address = pythonic_slave.slave_address
        c_slave.register_address_size = pythonic_slave.register_address_size
        c_slave.bus_index = pythonic_slave.bus_index
        return c_slave

    def i2c_write(self, slave, register_address, data):
        """Write data to an I2C slave.

        Args:
            slave (:class:`hailo_platform.pyhailort.i2c_slaves.I2CSlave`): I2C slave
                configuration.
            register_address (int): The address of the register to which the data will be written.
            data (str): The data that will be written.
        """
        c_slave = self._create_c_i2c_slave(slave)
        with ExceptionWrapper():
            return self._device.i2c_write(c_slave, register_address, data, len(data))

    def i2c_read(self, slave, register_address, data_length):
        """Read data from an I2C slave.

        Args:
            slave (:class:`hailo_platform.pyhailort.i2c_slaves.I2CSlave`): I2C slave
                configuration.
            register_address (int): The address of the register from which the data will be read.
            data_length (int): The number of bytes to read.

        Returns:
            str: Data read from the I2C slave.
        """
        c_slave = self._create_c_i2c_slave(slave)
        with ExceptionWrapper():
            return self._device.i2c_read(c_slave, register_address, data_length)

    def read_register(self, address):
        """Read the value of a register from a given address.

        Args:
            address (int): Address to read register from.

        Returns:
            int: Value of the register
        """
        register_value, = struct.unpack('!I', self.read_memory(address, type(self).WORD_SIZE))
        return register_value

    def set_bit(self, address, bit_index):
        """Set (turn on) a specific bit at a register from a given address.

        Args:
            address (int) : Address of the register to modify.
            bit_index (int) : Index of the bit that would be set.
        """
        register_value = self.read_register(address)
        register_value |= 1 << bit_index
        self.write_memory(address, struct.pack('!I', register_value))

    def reset_bit(self, address, bit_index):
        """Reset (turn off) a specific bit at a register from a given address.

        Args:
            address (int) :  Address of the register to modify.
            bit_index (int) : Index of the bit that would be reset.
        """
        register_value = self.read_register(address)
        register_value &= ~(1 << bit_index)
        self.write_memory(address, struct.pack('!I', register_value))

    def firmware_update(self, firmware_binary, should_reset=True):
        """Update firmware binary on the flash. 
        
        Args:
            firmware_binary (bytes): firmware binary stream.
            should_reset (bool): Should a reset be performed after the update (to load the new firmware)
        """
        with ExceptionWrapper():
            return self._device.firmware_update(firmware_binary, len(firmware_binary), should_reset)

    def second_stage_update(self, second_stage_binary):
        """Update second stage binary on the flash
        
        Args:
            second_stage_binary (bytes): second stage binary stream.
        """
        with ExceptionWrapper():
            return self._device.second_stage_update(second_stage_binary, len(second_stage_binary))

    def store_sensor_config(self, section_index, reset_data_size, sensor_type, config_file_path,
                            config_height=0, config_width=0, config_fps=0, config_name=None):
            
        """Store sensor configuration to Hailo chip flash memory.
        
        Args:
            section_index (int): Flash section index to write to. [0-6]
            reset_data_size (int): Size of reset configuration.
            sensor_type (:class:`~hailo_platform.pyhailort.pyhailort.SensorConfigTypes`): Sensor type.
            config_file_path (str): Sensor configuration file path.
            config_height (int): Configuration resolution height.
            config_width (int): Configuration resolution width.
            config_fps (int): Configuration FPS.
            config_name (str): Sensor configuration name.
        """
        if config_name is None:
            config_name = "UNINITIALIZED"

        with ExceptionWrapper():
            return self._device.sensor_store_config(section_index, reset_data_size, sensor_type, config_file_path,
            config_height, config_width, config_fps, config_name)

    def store_isp_config(self, reset_config_size, isp_static_config_file_path, isp_runtime_config_file_path,
                         config_height=0, config_width=0, config_fps=0, config_name=None):
        """Store sensor isp configuration to Hailo chip flash memory.

        Args:
            reset_config_size (int): Size of reset configuration.
            isp_static_config_file_path (str): Sensor isp static configuration file path.
            isp_runtime_config_file_path (str): Sensor isp runtime configuration file path.
            config_height (int): Configuration resolution height.
            config_width (int): Configuration resolution width.
            config_fps (int): Configuration FPS.
            config_name (str): Sensor configuration name.
        """
        if config_name is None:
            config_name = "UNINITIALIZED"

        with ExceptionWrapper():
            return self._device.store_isp_config(reset_config_size, config_height, config_width, 
            config_fps, isp_static_config_file_path, isp_runtime_config_file_path, config_name)

    def get_sensor_sections_info(self):
        """Get sensor sections info from Hailo chip flash memory.

        Returns:
            Sensor sections info read from the chip flash memory.
        """
        with ExceptionWrapper():
            return self._device.sensor_get_sections_info()

    def sensor_set_generic_i2c_slave(self, slave_address, register_address_size, bus_index, should_hold_bus, endianness):
        """Set a generic I2C slave for sensor usage.

        Args:
            sequence (int): Request/response sequence.
            slave_address (int): The address of the I2C slave.
            register_address_size (int): The size of the offset (in bytes).
            bus_index (int): The number of the bus the I2C slave is behind.
            should_hold_bus (bool): Hold the bus during the read.
            endianness (:class:`~hailo_platform.pyhailort.pyhailort.Endianness`):
                Big or little endian.
        """
        with ExceptionWrapper():
            return self._device.sensor_set_generic_i2c_slave(slave_address, register_address_size, bus_index, should_hold_bus, endianness)

    def set_sensor_i2c_bus_index(self, sensor_type, i2c_bus_index):
        """Set the I2C bus to which the sensor of the specified type is connected.
  
        Args:
            sensor_type (:class:`~hailo_platform.pyhailort.pyhailort.SensorConfigTypes`): The sensor type.
            i2c_bus_index (int): The I2C bus index of the sensor.
        """
        with ExceptionWrapper():
            return self._device.sensor_set_i2c_bus_index(sensor_type, i2c_bus_index)

    def load_and_start_sensor(self, section_index):
        """Load the configuration with I2C in the section index.
  
        Args:
            section_index (int): Flash section index to load config from. [0-6]
        """
        with ExceptionWrapper():
            return self._device.sensor_load_and_start_config(section_index)

    def reset_sensor(self, section_index):
        """Reset the sensor that is related to the section index config.

        Args:
            section_index (int): Flash section index to reset. [0-6]
        """
        with ExceptionWrapper():
            return self._device.sensor_reset(section_index)

    def wd_enable(self, cpu_id):
        """Enable firmware watchdog.

        Args:
            cpu_id (:class:`~hailo_platform.pyhailort.pyhailort.HailoCpuId`): 0 for App CPU, 1 for Core CPU.
        """
        with ExceptionWrapper():
            return self._device.wd_enable(cpu_id)

    def wd_disable(self, cpu_id):
        """Disable firmware watchdog.

        Args:
            cpu_id (:class:`~hailo_platform.pyhailort.pyhailort.HailoCpuId`): 0 for App CPU, 1 for Core CPU.
        """
        with ExceptionWrapper():
            return self._device.wd_disable(cpu_id)

    def wd_config(self, cpu_id, wd_cycles, wd_mode):
        """Configure a firmware watchdog.

        Args:
            cpu_id (:class:`~hailo_platform.pyhailort.pyhailort.HailoCpuId`): 0 for App CPU, 1 for Core CPU.
            wd_cycles (int): number of cycles until watchdog is triggered.
            wd_mode (int): 0 - HW/SW mode, 1 -  HW only mode
        """
        with ExceptionWrapper():
            return self._device.wd_config(cpu_id, wd_cycles, wd_mode)

    def previous_system_state(self, cpu_id):
        """Read the FW previous system state.

        Args:
            cpu_id (:class:`~hailo_platform.pyhailort.pyhailort.HailoCpuId`): 0 for App CPU, 1 for Core CPU.
        """
        with ExceptionWrapper():
            return self._device.previous_system_state(cpu_id)

    def get_chip_temperature(self):
        """Returns the latest temperature measurements from the 2 internal temperature sensors of the Hailo chip.

        Returns:
            :class:`~hailo_platform.pyhailort.pyhailort.TemperatureInfo`:
             Temperature in celsius of the 2 internal temperature sensors (TS), and a sample
             count (a running 16-bit counter)
        """
        with ExceptionWrapper():
            return self._device.get_chip_temperature()

    def get_extended_device_information(self):
        """Returns extended information about the device

        Returns:
            :class:`~hailo_platform.pyhailort.pyhailort.ExtendedDeviceInformation`:

        """
        with ExceptionWrapper():
            response = self._device.get_extended_device_information()
        device_information = ExtendedDeviceInformation(response.neural_network_core_clock_rate,
            response.supported_features, response.boot_source, response.lcs, response.soc_id,  response.eth_mac_address , response.unit_level_tracking_id, response.soc_pm_values)
        return device_information

    def _get_health_information(self):
        with ExceptionWrapper():
            response = self._device._get_health_information()

        health_information = HealthInformation(response.overcurrent_protection_active, response.current_overcurrent_zone, response.red_overcurrent_threshold,
                    response.overcurrent_throttling_active, response.temperature_throttling_active, response.current_temperature_zone, response.current_temperature_throttling_level, 
                    response.temperature_throttling_levels, response.orange_temperature_threshold, response.orange_hysteresis_temperature_threshold,
                    response.red_temperature_threshold, response.red_hysteresis_temperature_threshold, response.requested_overcurrent_clock_freq,
                    response.requested_temperature_clock_freq)
        return health_information

    def set_pause_frames(self, rx_pause_frames_enable):
        """Enable/Disable Pause frames.

        Args:
            rx_pause_frames_enable (bool): False for disable, True for enable.
        """
        with ExceptionWrapper():
            return self._device.set_pause_frames(rx_pause_frames_enable)

    def test_chip_memories(self):
        """test all chip memories using smart BIST

        """
        with ExceptionWrapper():
            return self._device.test_chip_memories()

    def set_notification_callback(self, callback_func, notification_id, opaque):
        """Set a callback function to be called when a notification is received.

        Args:
            callback_func (function): Callback function with the parameters (device, notification, opaque).
                Note that throwing exceptions is not supported and will cause the program to terminate with an error!
            notification_id (NotificationId): Notification ID to register the callback to.
            opauqe (object): User defined data.

        Note:
            The notifications thread is started and closed in the use_device() context, so
            notifications can only be received there.
        """
        with ExceptionWrapper():
            return self._device.set_notification_callback(callback_func, notification_id, opaque)

    def remove_notification_callback(self, notification_id):
        """Remove a notification callback which was already set.

        Args:
            notification_id (NotificationId): Notification ID to remove the callback from.
        """
        with ExceptionWrapper():
            return self._device.remove_notification_callback(notification_id)

    def _get_device_handle(self):
        return self._device


class Device:
    """ Hailo device object representation (for inference use VDevice)"""

    @classmethod
    def scan(cls):
        """Scans for all devices on the system.

        Returns:
            list of str, device ids.
        """
        return _pyhailort.Device.scan()

    def __init__(self, device_id=None):
        """Create the Hailo device object.

        Args:
            device_id (str): Device id string, can represent several device types:
                                [-] for pcie devices - pcie bdf (XXXX:XX:XX.X)
                                [-] for ethernet devices - ip address (xxx.xxx.xxx.xxx)
        """
        gc.collect()

        # Device __del__ function tries to release self._device.
        # to avoid AttributeError if the __init__ func fails, we set it to None first.
        # https://stackoverflow.com/questions/6409644/is-del-called-on-an-object-that-doesnt-complete-init
        self._device = None

        if device_id is None:
            system_device_ids = Device.scan()
            if len(system_device_ids) == 0:
                raise HailoRTException('HailoRT device not found in the system')
            device_id = system_device_ids[0]

        self._device_id = device_id
        self._device = _pyhailort.Device.create(device_id)
        self._loaded_network_groups = []
        self._creation_pid = os.getpid()
        self._control_object = Control(self._device)

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.release()
        return False

    def __del__(self):
        self.release()

    def __repr__(self) -> str:
        return f'Device({self._device_id!r})'

    def release(self):
        """Release the allocated resources of the device. This function should be called when working with the device not as context-manager."""

        if self._device is not None:
            with ExceptionWrapper():
                self._device.release()
            self._device = None

    @property
    def device_id(self):
        """Getter for the device_id.

        Returns:
            str: A string ID of the device. BDF for PCIe devices, IP address for Ethernet devices, "Integrated" for integrated nnc devices.
        """
        return self._device_id

    def configure(self, hef, configure_params_by_name={}):
        """Configures target device from HEF object.

        Args:
            hef (:class:`~hailo_platform.pyhailort.pyhailort.HEF`): HEF to configure the vdevice from
            configure_params_by_name (dict, optional): Maps between each net_group_name to configure_params. If not provided, default params will be applied

        Note:
            This function is deprecated. Support will be removed in future versions.
        """
        default_logger().warning("Usage of Device.configure is deprecated! One should use VDevice for inference")
        if self._creation_pid != os.getpid():
            raise HailoRTException("Device can only be configured from the process it was created in.")
        with ExceptionWrapper():
            configured_ngs_handles = self._device.configure(hef._hef, configure_params_by_name)
        configured_networks = [ConfiguredNetwork(configured_ng_handle) for configured_ng_handle in configured_ngs_handles]
        self._loaded_network_groups.extend(configured_networks)
        return configured_networks

    @property
    def control(self):
        """
        Returns:
            :class:`~hailo_platform.pyhailort.pyhailort.Control`: the control object of this device, which
            implements the control API of the Hailo device.

        .. attention:: Use the low level control API with care.
        """
        return self._control_object

    def read_log(self, count, cpu_id):
        """
        Returns:
            Returns buffer with debug log data.

        Args:
            count (int): bytes count to read
            cpu_id (HailoCpuId): cpu id
        """
        with ExceptionWrapper():
            return self._device.read_log(count, cpu_id)

    @property
    def loaded_network_groups(self):
        """Getter for the property _loaded_network_groups.

        Returns:
            list of :obj:`ConfiguredNetwork`: List of the the configured network groups loaded on the device.
        """
        return self._loaded_network_groups

    @property
    def _loaded_network_group(self):
        if len(self._loaded_network_groups) != 1:
            raise HailoRTException("Access to network group is only allowed when there is a single loaded network group")
        return self._loaded_network_groups[0]


class VDevice(object):
    """Hailo virtual device representation."""

    def __init__(self, params=None, *, device_ids=None):

        """Create the Hailo virtual device object.

        Args:
            params (:obj:`hailo_platform.pyhailort.pyhailort.VDeviceParams`, optional): VDevice params, call
                :func:`VDevice.create_params` to get default params. Excludes 'device_ids'.
            device_ids (list of str, optional): devices ids to create VDevice from, call :func:`Device.scan` to get
                list of all available devices. Excludes 'params'. Cannot be used together with device_id.
        """
        gc.collect()

        # VDevice __del__ function tries to release self._vdevice.
        # to avoid AttributeError if the __init__ func fails, we set it to None first.
        # https://stackoverflow.com/questions/6409644/is-del-called-on-an-object-that-doesnt-complete-init
        self._vdevice = None

        self._id = "VDevice"
        self._params = params
        self._loaded_network_groups = []
        self._creation_pid = os.getpid()

        self._device_ids = device_ids

        self._open_vdevice()

    def _open_vdevice(self):
        if self._params is None:
            self._params = VDevice.create_params()
        with ExceptionWrapper():
            device_ids = [] if self._device_ids is None else self._device_ids
            self._vdevice = _pyhailort.VDevice.create(self._params, device_ids)

    def __enter__(self):
        return self

    def release(self):
        """Release the allocated resources of the device. This function should be called when working with the device not as context-manager."""
        if self._vdevice is not None:
            self._vdevice.release()
            self._vdevice = None

    def __exit__(self, *args):
        self.release()
        return False

    def __del__(self):
        self.release()

    @staticmethod
    def create_params():
        with ExceptionWrapper():
            return _pyhailort.VDeviceParams.default()

    def configure(self, hef, configure_params_by_name={}):
        """Configures target vdevice from HEF object.

        Args:
            hef (:class:`~hailo_platform.pyhailort.pyhailort.HEF`): HEF to configure the vdevice from
            configure_params_by_name (dict, optional): Maps between each net_group_name to configure_params. If not provided, default params will be applied
        """
        if self._creation_pid != os.getpid():
            raise HailoRTException("VDevice can only be configured from the process it was created in.")
        with ExceptionWrapper():
            configured_ngs_handles = self._vdevice.configure(hef._hef, configure_params_by_name)
        configured_networks = [ConfiguredNetwork(configured_ng_handle) for configured_ng_handle in configured_ngs_handles]
        self._loaded_network_groups.extend(configured_networks)
        return configured_networks

    def get_physical_devices(self):
        """Gets the underlying physical devices.

        Return:
            list of :obj:`~hailo_platform.pyhailort.pyhailort.Device`: The underlying physical devices.
        """
        phys_dev_infos = self.get_physical_devices_ids()
        return [Device(dev_id) for dev_id in phys_dev_infos]

    def get_physical_devices_ids(self):
        """Gets the physical devices ids.

        Return:
            list of :obj:`str`: The underlying physical devices infos.
        """
        with ExceptionWrapper():
            return self._vdevice.get_physical_devices_ids()

    @property
    def loaded_network_groups(self):
        """Getter for the property _loaded_network_groups.

        Returns:
            list of :obj:`ConfiguredNetwork`: List of the the configured network groups loaded on the device.
        """
        return self._loaded_network_groups

    @property
    def _loaded_network_group(self):
        if len(self._loaded_network_groups) != 1:
            raise HailoRTException("Access to network group is only allowed when there is a single loaded network group")
        return self._loaded_network_groups[0]


class InputVStreamParams(object):
    """Parameters of an input virtual stream (host to device)."""

    @staticmethod
    def make(configured_network, quantized=None, format_type=None, timeout_ms=None, queue_size=None, network_name=None):
        """Create input virtual stream params from a configured network group. These params determine the format of the
        data that will be fed into the network group.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which
                the params are created.
            quantized: Unused.
            format_type (:class:`~hailo_platform.pyhailort.pyhailort.FormatType`): The
                default format type of the data for all input virtual streams.
                The default is :attr:`~hailo_platform.pyhailort.pyhailort.FormatType.AUTO`,
                which means the data is fed in the same format expected by the device (usually
                uint8).
            timeout_ms (int): The default timeout in milliseconds for all input virtual streams.
                Defaults to DEFAULT_VSTREAM_TIMEOUT_MS. In case of timeout, :class:`HailoRTTimeout` will be raised.
            queue_size (int): The pipeline queue size. Defaults to DEFAULT_VSTREAM_QUEUE_SIZE.
            network_name (str): Network name of the requested virtual stream params.
                If not passed, all the networks in the network group will be addressed.

        Returns:
            dict: The created virtual streams params. The keys are the vstreams names. The values are the
            params.
        """
        if format_type is None:
            format_type = FormatType.AUTO
        if timeout_ms is None:
            timeout_ms = DEFAULT_VSTREAM_TIMEOUT_MS
        if queue_size is None:
            queue_size = DEFAULT_VSTREAM_QUEUE_SIZE
        name = network_name if network_name is not None else ""
        with ExceptionWrapper():
            return configured_network._configured_network.make_input_vstream_params(name, format_type, timeout_ms, queue_size)

    @staticmethod
    def make_from_network_group(configured_network, quantized=None, format_type=None, timeout_ms=None, queue_size=None, network_name=None):
        """Create input virtual stream params from a configured network group. These params determine the format of the
        data that will be fed into the network group.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which
                the params are created.
            quantized: Unused.
            format_type (:class:`~hailo_platform.pyhailort.pyhailort.FormatType`): The
                default format type of the data for all input virtual streams.
                The default is :attr:`~hailo_platform.pyhailort.pyhailort.FormatType.AUTO`,
                which means the data is fed in the same format expected by the device (usually
                uint8).
            timeout_ms (int): The default timeout in milliseconds for all input virtual streams.
                Defaults to DEFAULT_VSTREAM_TIMEOUT_MS. In case of timeout, :class:`HailoRTTimeout` will be raised.
            queue_size (int): The pipeline queue size. Defaults to DEFAULT_VSTREAM_QUEUE_SIZE.
            network_name (str): Network name of the requested virtual stream params.
                If not passed, all the networks in the network group will be addressed.

        Returns:
            dict: The created virtual streams params. The keys are the vstreams names. The values are the
            params.
        """
        return InputVStreamParams.make(configured_network=configured_network, format_type=format_type, timeout_ms=timeout_ms,
            queue_size=queue_size, network_name=network_name)


class OutputVStreamParams(object):
    """Parameters of an output virtual stream (device to host)."""

    @staticmethod
    def make(configured_network, quantized=None, format_type=None, timeout_ms=None, queue_size=None, network_name=None):
        """Create output virtual stream params from a configured network group. These params determine the format of the
        data that will be returned from the network group.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which
                the params are created.
            quantized: Unused.
            format_type (:class:`~hailo_platform.pyhailort.pyhailort.FormatType`): The
                default format type of the data for all output virtual streams.
                The default is :attr:`~hailo_platform.pyhailort.pyhailort.FormatType.AUTO`,
                which means the returned data is in the same format returned from the device (usually
                uint8).
            timeout_ms (int): The default timeout in milliseconds for all output virtual streams.
                Defaults to DEFAULT_VSTREAM_TIMEOUT_MS. In case of timeout, :class:`HailoRTTimeout` will be raised.
            queue_size (int): The pipeline queue size. Defaults to DEFAULT_VSTREAM_QUEUE_SIZE.
            network_name (str): Network name of the requested virtual stream params.
                If not passed, all the networks in the network group will be addressed.

        Returns:
            dict: The created virtual streams params. The keys are the vstreams names. The values are the
            params.
        """
        if format_type is None:
            format_type = FormatType.AUTO
        if timeout_ms is None:
            timeout_ms = DEFAULT_VSTREAM_TIMEOUT_MS
        if queue_size is None:
            queue_size = DEFAULT_VSTREAM_QUEUE_SIZE
        name = network_name if network_name is not None else ""
        with ExceptionWrapper():
            return configured_network._configured_network.make_output_vstream_params(name, format_type, timeout_ms, queue_size)

    @staticmethod
    def make_from_network_group(configured_network, quantized=None, format_type=None, timeout_ms=None, queue_size=None, network_name=None):
        """Create output virtual stream params from a configured network group. These params determine the format of the
        data that will be returned from the network group.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which
                the params are created.
            quantized: Unused.
            format_type (:class:`~hailo_platform.pyhailort.pyhailort.FormatType`): The
                default format type of the data for all output virtual streams.
                The default is :attr:`~hailo_platform.pyhailort.pyhailort.FormatType.AUTO`,
                which means the returned data is in the same format returned from the device (usually
                uint8).
            timeout_ms (int): The default timeout in milliseconds for all output virtual streams.
                Defaults to DEFAULT_VSTREAM_TIMEOUT_MS. In case of timeout, :class:`HailoRTTimeout` will be raised.
            queue_size (int): The pipeline queue size. Defaults to DEFAULT_VSTREAM_QUEUE_SIZE.
            network_name (str): Network name of the requested virtual stream params.
                If not passed, all the networks in the network group will be addressed.

        Returns:
            dict: The created virtual streams params. The keys are the vstreams names. The values are the
            params.
        """
        return OutputVStreamParams.make(configured_network=configured_network, format_type=format_type, timeout_ms=timeout_ms,
            queue_size=queue_size, network_name=network_name)

    @staticmethod
    def make_groups(configured_network, quantized=None, format_type=None, timeout_ms=None, queue_size=None):
        """Create output virtual stream params from a configured network group. These params determine the format of the
        data that will be returned from the network group. The params groups are splitted with respect to their underlying streams for multi process usges.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which
                the params are created.
            quantized: Unused.
            format_type (:class:`~hailo_platform.pyhailort.pyhailort.FormatType`): The
                default format type of the data for all output virtual streams.
                The default is :attr:`~hailo_platform.pyhailort.pyhailort.FormatType.AUTO`,
                which means the returned data is in the same format returned from the device (usually
                uint8).
            timeout_ms (int): The default timeout in milliseconds for all output virtual streams.
                Defaults to DEFAULT_VSTREAM_TIMEOUT_MS. In case of timeout, :class:`HailoRTTimeout` will be raised.
            queue_size (int): The pipeline queue size. Defaults to DEFAULT_VSTREAM_QUEUE_SIZE.

        Returns:
            list of dicts: Each element in the list represent a group of params, where the keys are the vstreams names, and the values are the
            params. The params groups are splitted with respect to their underlying streams for multi process usges.
        """
        all_params = OutputVStreamParams.make(configured_network=configured_network, format_type=format_type, timeout_ms=timeout_ms, queue_size=queue_size)
        low_level_streams_names = [stream_info.name for stream_info in configured_network.get_output_stream_infos()]
        stream_name_to_vstream_names = {stream_name: configured_network.get_vstream_names_from_stream_name(stream_name) for stream_name in low_level_streams_names}
        results = []
        for low_level_stream_name, vstream_names in stream_name_to_vstream_names.items():
            params_group = {}
            for vstream_name in vstream_names:
                # Vstreams that were already seen should not be added to another params_group
                if all_params[vstream_name] is not None:
                    params_group[vstream_name] = all_params[vstream_name]
                    all_params[vstream_name] = None
            if 0 < len(params_group):
                results.append(params_group)
        return results


class InputVStream(object):
    """Represents a single virtual stream in the host to device direction."""

    def __init__(self, send_object):
        self._send_object = send_object
        self._input_dtype = self._send_object.dtype

    @property
    def shape(self):
        return self._send_object.shape
    
    @property
    def dtype(self):
        return self._send_object.dtype

    @property
    def name(self):
        return self._send_object.info.name

    @property
    def network_name(self):
        return self._send_object.info.network_name

    def send(self, input_data):
        """Send frames to inference.

        Args:
            input_data (:obj:`numpy.ndarray`): Data to run inference on.
        """

        if input_data.dtype != self._input_dtype:
            input_data = input_data.astype(self._input_dtype)

        if not input_data.flags.c_contiguous:
            logger = default_logger()
            logger.warning("Warning - Converting input numpy array to be C_CONTIGUOUS")
            input_data = numpy.asarray(input_data, order='C')

        batch_number = 0
        batch_size = 1
        while batch_number < input_data.shape[0]:
            data = input_data[batch_number:batch_number + batch_size]
            with ExceptionWrapper():
                self._send_object.send(data)
            batch_number += batch_size

    def flush(self):
        """Blocks until there are no buffers in the input VStream pipeline."""
        with ExceptionWrapper():
            self._send_object.flush()

    @property
    def info(self):
        with ExceptionWrapper():
            return self._send_object.info

class InputVStreams(object):
    """Input vstreams pipelines that allows to send data, to be used as a context manager."""

    def __init__(self, configured_network, input_vstreams_params):
        """Constructor for the InputVStreams class.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which the pipeline is created.
            input_vstreams_params (dict from str to :class:`InputVStreamParams`): Params for the input vstreams in the pipeline.
        """
        self._configured_network = configured_network
        self._input_vstreams_params = input_vstreams_params
        self._vstreams = {}

    def __enter__(self):
        self._input_vstreams_holder = self._configured_network._create_input_vstreams(self._input_vstreams_params)
        self._input_vstreams_holder.__enter__()
        for name, vstream in self._input_vstreams_holder.get_all_inputs().items():
            self._vstreams[name] = InputVStream(vstream)
        return self

    def get(self, name=None):
        """Return a single input vstream by its name.
        
        Args:
            name (str): The vstream name. If name=None and there is a single input vstream, that single (:class:`InputVStream`) will be returned.
                Otherwise, if name=None and there are multiple input vstreams, an exception will be thrown.

        Returns:
            :class:`InputVStream`: The (:class:`InputVStream`) that corresponds to the given name.
        """
        if name is None:
            if len(self._vstreams) != 1:
                raise HailoRTException("There is no single input vStream. You must give a name")
            name = list(self._vstreams.keys())[0]
        return self._vstreams[name]

    def clear(self):
        """Clears the vstreams' pipeline buffers."""
        with ExceptionWrapper():
            self._input_vstreams_holder.clear()

    def __exit__(self, *args):
        self._input_vstreams_holder.__exit__(*args)
        return False

    def __iter__(self):
        return iter(self._vstreams.values())



class OutputLayerUtils(object):
    def __init__(self, output_vstream_infos, vstream_name, pipeline, net_group_name=""):
        self._output_vstream_infos = output_vstream_infos
        self._vstream_info = self._get_vstream_info(net_group_name, vstream_name)

        if isinstance(pipeline, (_pyhailort.InferVStreams)):
            self._user_buffer_format = pipeline.get_user_buffer_format(vstream_name)
            self._output_shape = pipeline.get_shape(vstream_name)
        else:
            self._user_buffer_format = pipeline.get_user_buffer_format()
            self._output_shape = pipeline.shape

        self._is_nms = (self._user_buffer_format.order == FormatOrder.HAILO_NMS)

        if self._is_nms:
            self._quantized_empty_bbox = numpy.asarray([0] * BBOX_PARAMS, dtype=self.output_dtype)
            if self.output_dtype == numpy.float32:
                HailoRTTransformUtils.dequantize_output_buffer_in_place(self._quantized_empty_bbox, self.output_dtype,
                    BBOX_PARAMS, self._vstream_info.quant_info)

    @property
    def output_dtype(self):
        return _pyhailort.get_dtype(self._user_buffer_format.type)

    @property
    def output_order(self):
        return self._user_buffer_format.order

    @property
    def output_shape(self):
        return self._output_shape

    @property
    def vstream_info(self):
        return self._vstream_info

    @property
    def output_tensor_info(self):
        return self.output_shape, self.output_dtype

    @property
    def is_nms(self):
        return self._is_nms
    
    @property
    def quantized_empty_bbox(self):
        if not self.is_nms:
            raise HailoRTException("Requested NMS info for non-NMS layer")
        return self._quantized_empty_bbox

    def _get_vstream_info(self, net_group_name, vstream_name):
        for info in self._output_vstream_infos:
            if info.name == vstream_name:
                return info
        raise HailoRTException("No vstream matches the given name {}".format(vstream_name))

    @property
    def tf_nms_fomrat_shape(self):
        # TODO: HRT-11726 - Combine is_nms for HAILO_NMS and NMS_WITH_BYTE_MASK
        if not self.is_nms and not self.output_order == FormatOrder.HAILO_NMS_WITH_BYTE_MASK:
            raise HailoRTException("Requested NMS info for non-NMS layer")
        nms_shape = self._vstream_info.nms_shape
        return [nms_shape.number_of_classes, BBOX_PARAMS,
                nms_shape.max_bboxes_per_class]

class OutputVStream(object):
    """Represents a single output virtual stream in the device to host direction."""

    def __init__(self, configured_network, recv_object, name, tf_nms_format=False, net_group_name=""):
        self._recv_object = recv_object
        output_vstream_infos = configured_network.get_output_vstream_infos()
        self._output_layer_utils = OutputLayerUtils(output_vstream_infos, name, self._recv_object, net_group_name)
        self._output_dtype = self._output_layer_utils.output_dtype
        self._vstream_info = self._output_layer_utils._vstream_info
        self._output_tensor_info = self._output_layer_utils.output_tensor_info
        self._is_nms = self._output_layer_utils.is_nms
        if self._is_nms:
            self._quantized_empty_bbox = self._output_layer_utils.quantized_empty_bbox
        self._tf_nms_format = tf_nms_format
        self._input_stream_infos = configured_network.get_input_stream_infos()

    @property
    def output_order(self):
        return self._output_layer_utils.output_order

    @property
    def shape(self):
        return self._recv_object.shape

    @property
    def dtype(self):
        return self._recv_object.dtype

    @property
    def name(self):
        return self._vstream_info.name

    @property
    def network_name(self):
        return self._vstream_info.network_name

    def recv(self):
        """Receive frames after inference.

        Returns:
            :obj:`numpy.ndarray`: The output of the inference for a single frame. The returned
            tensor does not include the batch dimension.
            In case of nms output and tf_nms_format=False, returns list of :obj:`numpy.ndarray`.
        """
        result_array = None
        with ExceptionWrapper():
            result_array = self._recv_object.recv()

        if self.output_order == FormatOrder.HAILO_NMS_WITH_BYTE_MASK:
            nms_shape = self._vstream_info.nms_shape
            if len(self._input_stream_infos) != 1:
                raise Exception("Output format HAILO_NMS_WITH_BYTE_MASK should have 1 input. Number of inputs: {}".format(len(self._input_stream_infos)))
            input_height = self._input_stream_infos[0].shape[0]
            input_width = self._input_stream_infos[0].shape[1]
            res = HailoRTTransformUtils._output_raw_buffer_to_nms_with_byte_mask_format(result_array,
                nms_shape.number_of_classes, 1, input_height, input_width, nms_shape.max_bboxes_per_class,
                self._output_dtype, self._tf_nms_format)
            return res

        if self._is_nms:
            nms_shape = self._vstream_info.nms_shape
            if self._tf_nms_format:
                nms_results_tesnor = result_array
                # We create the tf_format buffer with reversed width/features for preformance optimization
                shape = self._output_layer_utils.tf_nms_fomrat_shape
                result_array = numpy.empty([shape[0], shape[2], shape[1]], dtype=self._output_dtype)
                HailoRTTransformUtils.output_raw_buffer_to_nms_tf_format_single_frame(nms_results_tesnor, result_array,
                    nms_shape.number_of_classes,
                    nms_shape.max_bboxes_per_class, self._quantized_empty_bbox)
                result_array = numpy.swapaxes(result_array, 1, 2)
            else:
                result_array = HailoRTTransformUtils.output_raw_buffer_to_nms_format_single_frame(result_array,
                    nms_shape.number_of_classes)
        return result_array

    @property
    def info(self):
        with ExceptionWrapper():
            return self._recv_object.info

    def set_nms_score_threshold(self, threshold):
        """Set NMS score threshold, used for filtering out candidates. Any box with score<TH is suppressed.

        Args:
            threshold (float): NMS score threshold to set.

        Note:
            This function will fail in cases where the output vstream has no NMS operations on the CPU.
        """
        return self._recv_object.set_nms_score_threshold(threshold)

    def set_nms_iou_threshold(self, threshold):
        """Set NMS intersection over union overlap Threshold,
            used in the NMS iterative elimination process where potential duplicates of detected items are suppressed.

        Args:
            threshold (float): NMS IoU threshold to set.

        Note:
            This function will fail in cases where the output vstream has no NMS operations on the CPU.
        """
        return self._recv_object.set_nms_iou_threshold(threshold)

    def set_nms_max_proposals_per_class(self, max_proposals_per_class):
        """Set a limit for the maximum number of boxes per class.

        Args:
            max_proposals_per_class (int): NMS max proposals per class to set.

        Note:
            This function will fail in cases where the output vstream has no NMS operations on the CPU.
        """
        return self._recv_object.set_nms_max_proposals_per_class(max_proposals_per_class)


class OutputVStreams(object):
    """Output virtual streams pipelines that allows to receive data, to be used as a context manager."""

    def __init__(self, configured_network, output_vstreams_params, tf_nms_format=False):
        """Constructor for the OutputVStreams class.

        Args:
            configured_network (:class:`ConfiguredNetwork`): The configured network group for which
                the pipeline is created.
            output_vstreams_params (dict from str to :class:`OutputVStreamParams`): Params for the
                output vstreams in the pipeline.
            tf_nms_format (bool, optional): indicates whether the returned nms outputs should be in
                Hailo format or TensorFlow format. Default is False (using Hailo format).

                * Hailo format -- list of :obj:`numpy.ndarray`. Each element represents th
                  detections (bboxes) for the class, and its shape is
                  ``[number_of_detections, BBOX_PARAMS]``
                * TensorFlow format -- :obj:`numpy.ndarray` of shape
                  ``[class_count, BBOX_PARAMS, detections_count]`` padded with empty bboxes.
        """
        self._configured_network = configured_network
        self._net_group_name = configured_network.name
        self._output_vstreams_params = output_vstreams_params
        self._output_tensor_info = {}
        self._tf_nms_format = tf_nms_format
        self._vstreams = {}

    def __enter__(self):
        self._output_vstreams_holder = self._configured_network._create_output_vstreams(self._output_vstreams_params)
        self._output_vstreams_holder.__enter__()
        for name, vstream in self._output_vstreams_holder.get_all_outputs().items():
            self._vstreams[name] = OutputVStream(self._configured_network, vstream, name,
                tf_nms_format=self._tf_nms_format, net_group_name=self._net_group_name)
        return self

    def get(self, name=None):
        """Return a single output vstream by its name.
        
        Args:
            name (str): The vstream name. If name=None and there is a single output vstream, that single (:class:`OutputVStream`) will be returned.
                Otherwise, if name=None and there are multiple output vstreams, an exception will be thrown.

        Returns:
            :class:`OutputVStream`: The (:class:`OutputVStream`) that corresponds to the given name.
        """
        if name is None:
            if len(self._vstreams) != 1:
                raise HailoRTException("There is no single output vStream. You must give a name")
            name = list(self._vstreams.keys())[0]
        return self._vstreams[name]

    def clear(self):
        """Clears the vstreams' pipeline buffers."""
        with ExceptionWrapper():
            self._output_vstreams_holder.clear()

    def __exit__(self, *args):
        self._output_vstreams_holder.__exit__(*args)
        return False

    def __iter__(self):
        return iter(self._vstreams.values())
