From ab24745b4788a53a89b4ca5e516811521dd36a00 Mon Sep 17 00:00:00 2001 From: John Ingve Olsen Date: Tue, 4 Jun 2024 16:35:13 +0200 Subject: [PATCH] Add network configuration This commit adds a wrapper for NetworkConfiguration datamodel as well as wrappers for new Camera class methods `network_configuration` and `apply_network_configuration`. --- .../datamodel_frontend_generator.py | 1 + modules/_zivid/__init__.py | 1 + modules/zivid/__init__.py | 1 + modules/zivid/camera.py | 50 ++++- modules/zivid/camera_state.py | 2 + modules/zivid/network_configuration.py | 194 ++++++++++++++++++ samples/sample_network_configuration.py | 65 ++++++ src/ReleasableCamera.cpp | 4 +- src/Wrapper.cpp | 1 + src/include/ZividPython/ReleasableCamera.h | 2 + test/test_network_configuration.py | 67 ++++++ 11 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 modules/zivid/network_configuration.py create mode 100644 samples/sample_network_configuration.py create mode 100644 test/test_network_configuration.py diff --git a/continuous-integration/code-generation/datamodel_frontend_generator.py b/continuous-integration/code-generation/datamodel_frontend_generator.py index 360f66c1..c852f4ab 100644 --- a/continuous-integration/code-generation/datamodel_frontend_generator.py +++ b/continuous-integration/code-generation/datamodel_frontend_generator.py @@ -714,6 +714,7 @@ def generate_all_datamodels(dest_dir: Path) -> None: ["datetime"], ), (_zivid.CameraIntrinsics, "camera_intrinsics.py", []), + (_zivid.NetworkConfiguration, "network_configuration.py", []), ]: _generate_datamodel_frontend( internal_class=internal_class, diff --git a/modules/_zivid/__init__.py b/modules/_zivid/__init__.py index 34ca221a..71b5e78e 100644 --- a/modules/_zivid/__init__.py +++ b/modules/_zivid/__init__.py @@ -64,6 +64,7 @@ CameraInfo, infield_correction, Matrix4x4, + NetworkConfiguration, data_model, PixelMapping, projection, diff --git a/modules/zivid/__init__.py b/modules/zivid/__init__.py index 5fc0d2f3..07d8ad02 100644 --- a/modules/zivid/__init__.py +++ b/modules/zivid/__init__.py @@ -24,3 +24,4 @@ from zivid.camera_info import CameraInfo from zivid.camera_intrinsics import CameraIntrinsics from zivid.matrix4x4 import Matrix4x4 +from zivid.network_configuration import NetworkConfiguration diff --git a/modules/zivid/camera.py b/modules/zivid/camera.py index afa626e2..a00bdb7c 100644 --- a/modules/zivid/camera.py +++ b/modules/zivid/camera.py @@ -1,11 +1,13 @@ """Contains Camera class.""" import _zivid +from zivid.camera_info import _to_camera_info +from zivid.camera_state import _to_camera_state from zivid.frame import Frame from zivid.frame_2d import Frame2D +from zivid.network_configuration import NetworkConfiguration, _to_internal_network_configuration, \ + _to_network_configuration from zivid.settings import Settings, _to_internal_settings from zivid.settings_2d import Settings2D, _to_internal_settings2d -from zivid.camera_info import _to_camera_info -from zivid.camera_state import _to_camera_state class Camera: @@ -90,6 +92,50 @@ def disconnect(self): """Disconnect from the camera and free all resources associated with it.""" self.__impl.disconnect() + @property + def network_configuration(self): + """Get the network configuration of the camera. + + Returns: + NetworkConfiguration instance + """ + return _to_network_configuration(self.__impl.network_configuration) + + def apply_network_configuration(self, network_configuration): + """ + Applies the given network configuration to the camera. + + Args: + network_configuration (NetworkConfiguration): The network configuration to apply to the camera. + + This method blocks until the camera has finished applying the network configuration, or raises an exception if + the camera does not reappear on the network before a timeout occurs. + + This method can be used even if the camera is inaccessible via TCP/IP, for example a camera that is on a + different subnet to the PC, or a camera with an IP conflict, as it uses UDP multicast to communicate with the + camera. + + This method can also be used to configure cameras that require a firmware update, as long as the firmware + supports network configuration via UDP multicast. This has been supported on all firmware versions included + with SDK 2.10.0 or newer. This method will raise an exception if the camera firmware is too old to support + UDP multicast. + + This method will raise an exception if the camera status (see CameraState.Status) is "busy", "connected", + "connecting" or "disconnecting". If the status is "connected", then you must first call disconnect() before + calling this method. + + Raises: + TypeError: If the provided network_configuration is not an instance of NetworkConfiguration. + """ + if not isinstance(network_configuration, NetworkConfiguration): + raise TypeError( + "Unsupported type, expected: {expected_type}, got: {value_type}".format( + expected_type=NetworkConfiguration, + value_type=type(network_configuration), + ) + ) + self.__impl.apply_network_configuration(_to_internal_network_configuration(network_configuration)) + def write_user_data(self, user_data): """Write user data to camera. The total number of writes supported depends on camera model and size of data. diff --git a/modules/zivid/camera_state.py b/modules/zivid/camera_state.py index 3b0ab51d..f989da6a 100644 --- a/modules/zivid/camera_state.py +++ b/modules/zivid/camera_state.py @@ -291,6 +291,7 @@ def valid_values(cls): return list(cls._valid_values.keys()) class Status: + applyingNetworkConfiguration = "applyingNetworkConfiguration" available = "available" busy = "busy" connected = "connected" @@ -302,6 +303,7 @@ class Status: updatingFirmware = "updatingFirmware" _valid_values = { + "applyingNetworkConfiguration": _zivid.CameraState.Status.applyingNetworkConfiguration, "available": _zivid.CameraState.Status.available, "busy": _zivid.CameraState.Status.busy, "connected": _zivid.CameraState.Status.connected, diff --git a/modules/zivid/network_configuration.py b/modules/zivid/network_configuration.py new file mode 100644 index 00000000..209a0e58 --- /dev/null +++ b/modules/zivid/network_configuration.py @@ -0,0 +1,194 @@ +"""Auto generated, do not edit.""" +# pylint: disable=too-many-lines,protected-access,too-few-public-methods,too-many-arguments,line-too-long,missing-function-docstring,missing-class-docstring,redefined-builtin,too-many-branches,too-many-boolean-expressions +import _zivid + + +class NetworkConfiguration: + class IPV4: + class Mode: + dhcp = "dhcp" + manual = "manual" + + _valid_values = { + "dhcp": _zivid.NetworkConfiguration.IPV4.Mode.dhcp, + "manual": _zivid.NetworkConfiguration.IPV4.Mode.manual, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + address=_zivid.NetworkConfiguration.IPV4.Address().value, + mode=_zivid.NetworkConfiguration.IPV4.Mode().value, + subnet_mask=_zivid.NetworkConfiguration.IPV4.SubnetMask().value, + ): + if isinstance(address, (str,)): + self._address = _zivid.NetworkConfiguration.IPV4.Address(address) + else: + raise TypeError( + "Unsupported type, expected: (str,), got {value_type}".format( + value_type=type(address) + ) + ) + + if isinstance(mode, _zivid.NetworkConfiguration.IPV4.Mode.enum): + self._mode = _zivid.NetworkConfiguration.IPV4.Mode(mode) + elif isinstance(mode, str): + self._mode = _zivid.NetworkConfiguration.IPV4.Mode( + self.Mode._valid_values[mode] + ) + else: + raise TypeError( + "Unsupported type, expected: str, got {value_type}".format( + value_type=type(mode) + ) + ) + + if isinstance(subnet_mask, (str,)): + self._subnet_mask = _zivid.NetworkConfiguration.IPV4.SubnetMask( + subnet_mask + ) + else: + raise TypeError( + "Unsupported type, expected: (str,), got {value_type}".format( + value_type=type(subnet_mask) + ) + ) + + @property + def address(self): + return self._address.value + + @property + def mode(self): + if self._mode.value is None: + return None + for key, internal_value in self.Mode._valid_values.items(): + if internal_value == self._mode.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._mode)) + + @property + def subnet_mask(self): + return self._subnet_mask.value + + @address.setter + def address(self, value): + if isinstance(value, (str,)): + self._address = _zivid.NetworkConfiguration.IPV4.Address(value) + else: + raise TypeError( + "Unsupported type, expected: str, got {value_type}".format( + value_type=type(value) + ) + ) + + @mode.setter + def mode(self, value): + if isinstance(value, str): + self._mode = _zivid.NetworkConfiguration.IPV4.Mode( + self.Mode._valid_values[value] + ) + elif isinstance(value, _zivid.NetworkConfiguration.IPV4.Mode.enum): + self._mode = _zivid.NetworkConfiguration.IPV4.Mode(value) + else: + raise TypeError( + "Unsupported type, expected: str, got {value_type}".format( + value_type=type(value) + ) + ) + + @subnet_mask.setter + def subnet_mask(self, value): + if isinstance(value, (str,)): + self._subnet_mask = _zivid.NetworkConfiguration.IPV4.SubnetMask(value) + else: + raise TypeError( + "Unsupported type, expected: str, got {value_type}".format( + value_type=type(value) + ) + ) + + def __eq__(self, other): + if ( + self._address == other._address + and self._mode == other._mode + and self._subnet_mask == other._subnet_mask + ): + return True + return False + + def __str__(self): + return str(_to_internal_network_configuration_ipv4(self)) + + def __init__( + self, + ipv4=None, + ): + if ipv4 is None: + ipv4 = self.IPV4() + if not isinstance(ipv4, self.IPV4): + raise TypeError("Unsupported type: {value}".format(value=type(ipv4))) + self._ipv4 = ipv4 + + @property + def ipv4(self): + return self._ipv4 + + @ipv4.setter + def ipv4(self, value): + if not isinstance(value, self.IPV4): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._ipv4 = value + + @classmethod + def load(cls, file_name): + return _to_network_configuration(_zivid.NetworkConfiguration(str(file_name))) + + def save(self, file_name): + _to_internal_network_configuration(self).save(str(file_name)) + + def __eq__(self, other): + if self._ipv4 == other._ipv4: + return True + return False + + def __str__(self): + return str(_to_internal_network_configuration(self)) + + +def _to_network_configuration_ipv4(internal_ipv4): + return NetworkConfiguration.IPV4( + address=internal_ipv4.address.value, + mode=internal_ipv4.mode.value, + subnet_mask=internal_ipv4.subnet_mask.value, + ) + + +def _to_network_configuration(internal_network_configuration): + return NetworkConfiguration( + ipv4=_to_network_configuration_ipv4(internal_network_configuration.ipv4), + ) + + +def _to_internal_network_configuration_ipv4(ipv4): + internal_ipv4 = _zivid.NetworkConfiguration.IPV4() + + internal_ipv4.address = _zivid.NetworkConfiguration.IPV4.Address(ipv4.address) + internal_ipv4.mode = _zivid.NetworkConfiguration.IPV4.Mode(ipv4._mode.value) + internal_ipv4.subnet_mask = _zivid.NetworkConfiguration.IPV4.SubnetMask( + ipv4.subnet_mask + ) + + return internal_ipv4 + + +def _to_internal_network_configuration(network_configuration): + internal_network_configuration = _zivid.NetworkConfiguration() + + internal_network_configuration.ipv4 = _to_internal_network_configuration_ipv4( + network_configuration.ipv4 + ) + return internal_network_configuration diff --git a/samples/sample_network_configuration.py b/samples/sample_network_configuration.py new file mode 100644 index 00000000..2d7f95e1 --- /dev/null +++ b/samples/sample_network_configuration.py @@ -0,0 +1,65 @@ +import zivid + + +def confirm(message): + while True: + input_value = input(f"{message} [Y/n] ") + if input_value.lower() in ["y", "yes"]: + return True + elif input_value.lower() in ["n", "no"]: + return False + + +def main(): + try: + app = zivid.Application() + camera = app.cameras()[0] + + original_config = camera.network_configuration + + print(f"Current network configuration of camera {camera.info.serial_number}:") + print(original_config) + print() + + mode = zivid.NetworkConfiguration.IPV4.Mode.manual + address = original_config.ipv4.address + subnet_mask = original_config.ipv4.subnet_mask + + if confirm("Do you want to use DHCP?"): + mode = zivid.NetworkConfiguration.IPV4.Mode.dhcp + else: + input_address = input(f"Enter IPv4 Address [{original_config.ipv4.address}]: ") + address = input_address if input_address else original_config.ipv4.address + input_subnet_mask = input(f"Enter new Subnet mask [{original_config.ipv4.subnet_mask}]: ") + subnet_mask = input_subnet_mask if input_subnet_mask else original_config.ipv4.subnet_mask + + new_config = zivid.NetworkConfiguration( + ipv4=zivid.NetworkConfiguration.IPV4( + mode=mode, + address=address, + subnet_mask=subnet_mask, + ) + ) + + print() + print("New network configuration:") + print(new_config) + if not confirm(f"Do you want to apply the new network configuration to camera {camera.info.serial_number}?"): + return 0 + + print("Applying network configuration...") + camera.apply_network_configuration(new_config) + + print(f"Updated network configuration of camera {camera.info.serial_number}:") + print(camera.network_configuration) + print() + + print(f"Camera status is '{camera.state.status}'") + except Exception as ex: + print(f"Error: {str(ex)}") + return 1 + return 0 + + +if __name__ == "__main__": + main() diff --git a/src/ReleasableCamera.cpp b/src/ReleasableCamera.cpp index 60957cc9..d0386144 100644 --- a/src/ReleasableCamera.cpp +++ b/src/ReleasableCamera.cpp @@ -24,6 +24,8 @@ namespace ZividPython .def_property_readonly("state", &ReleasableCamera::state) .def_property_readonly("info", &ReleasableCamera::info) .def("write_user_data", &ReleasableCamera::writeUserData) - .def_property_readonly("user_data", &ReleasableCamera::userData); + .def_property_readonly("user_data", &ReleasableCamera::userData) + .def_property_readonly("network_configuration", &ReleasableCamera::networkConfiguration) + .def("apply_network_configuration", &ReleasableCamera::applyNetworkConfiguration); } } // namespace ZividPython diff --git a/src/Wrapper.cpp b/src/Wrapper.cpp index b8426ffa..dac42240 100644 --- a/src/Wrapper.cpp +++ b/src/Wrapper.cpp @@ -38,6 +38,7 @@ ZIVID_PYTHON_MODULE // NOLINT ZIVID_PYTHON_WRAP_DATA_MODEL(module, CameraInfo); ZIVID_PYTHON_WRAP_DATA_MODEL(module, FrameInfo); ZIVID_PYTHON_WRAP_DATA_MODEL(module, CameraIntrinsics); + ZIVID_PYTHON_WRAP_DATA_MODEL(module, NetworkConfiguration); ZIVID_PYTHON_WRAP_CLASS_AS_SINGLETON(module, Application); ZIVID_PYTHON_WRAP_CLASS_AS_RELEASABLE(module, Camera); diff --git a/src/include/ZividPython/ReleasableCamera.h b/src/include/ZividPython/ReleasableCamera.h index 4b5910be..20dfbebc 100644 --- a/src/include/ZividPython/ReleasableCamera.h +++ b/src/include/ZividPython/ReleasableCamera.h @@ -23,6 +23,8 @@ namespace ZividPython ZIVID_PYTHON_FORWARD_0_ARGS(info) ZIVID_PYTHON_FORWARD_1_ARGS(writeUserData, const std::vector &, data) ZIVID_PYTHON_FORWARD_0_ARGS(userData) + ZIVID_PYTHON_FORWARD_0_ARGS(networkConfiguration) + ZIVID_PYTHON_FORWARD_1_ARGS(applyNetworkConfiguration, const Zivid::NetworkConfiguration &, networkConfiguration) }; void wrapClass(pybind11::class_ pyClass); diff --git a/test/test_network_configuration.py b/test/test_network_configuration.py new file mode 100644 index 00000000..fe1db7c1 --- /dev/null +++ b/test/test_network_configuration.py @@ -0,0 +1,67 @@ +import pytest +import zivid + + +class ScopeExit: + def __init__(self, exit_func): + self.exit_func = exit_func + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.exit_func() + + +class DisconnectCamera(ScopeExit): + def __init__(self, camera): + camera.disconnect() + super().__init__(lambda: camera.connect()) + + +class RestoreNetworkConfiguration(ScopeExit): + def __init__(self, camera): + network_configuration = camera.network_configuration + super().__init__(lambda: camera.apply_network_configuration(network_configuration)) + + +def test_default_network_configuration(): + network_configuration = zivid.NetworkConfiguration() + assert network_configuration.ipv4.mode == zivid.NetworkConfiguration.IPV4.Mode.manual + assert network_configuration.ipv4.address == "172.28.60.5" + assert network_configuration.ipv4.subnet_mask == "255.255.255.0" + + +@pytest.mark.physical_camera +def test_fetch_network_configuration_while_connected(physical_camera): + network_configuration = physical_camera.network_configuration + assert network_configuration is not None + assert isinstance(network_configuration, zivid.NetworkConfiguration) + assert network_configuration.ipv4.mode in zivid.NetworkConfiguration.IPV4.Mode.valid_values() + assert network_configuration.ipv4.address + assert network_configuration.ipv4.subnet_mask + + +@pytest.mark.physical_camera +def test_fetch_network_configuration_while_not_connected(physical_camera): + with DisconnectCamera(physical_camera): + network_configuration = physical_camera.network_configuration + assert network_configuration is not None + assert isinstance(network_configuration, zivid.NetworkConfiguration) + assert network_configuration.ipv4.mode in zivid.NetworkConfiguration.IPV4.Mode.valid_values() + assert network_configuration.ipv4.address + assert network_configuration.ipv4.subnet_mask + + +@pytest.mark.physical_camera +def test_apply_network_configuration_fails_while_connected(physical_camera): + with pytest.raises(RuntimeError): + physical_camera.apply_network_configuration(zivid.NetworkConfiguration()) + + +@pytest.mark.physical_camera +def test_apply_default_network_configuration(physical_camera): + with DisconnectCamera(physical_camera): + with RestoreNetworkConfiguration(physical_camera): + physical_camera.apply_network_configuration(zivid.NetworkConfiguration()) + assert physical_camera.network_configuration == zivid.NetworkConfiguration()