From fc4a81a29ef249ee425f8d7353f71c8e8f415e55 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`. --- .flake8 | 1 + .pylintrc-tests | 1 + .../datamodel_frontend_generator.py | 1 + modules/_zivid/__init__.py | 1 + modules/zivid/__init__.py | 1 + modules/zivid/camera.py | 55 ++++- modules/zivid/network_configuration.py | 203 ++++++++++++++++++ samples/sample_network_configuration.py | 65 ++++++ src/ReleasableCamera.cpp | 4 +- src/Wrapper.cpp | 1 + src/include/ZividPython/ReleasableCamera.h | 4 + .../datamodels/NetworkConfiguration.yml | 8 + ...est_datamodel_serialize_from_serialized.py | 1 + test/test_network_configuration.py | 77 +++++++ 14 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 modules/zivid/network_configuration.py create mode 100644 samples/sample_network_configuration.py create mode 100644 test/test_data/datamodels/NetworkConfiguration.yml create mode 100644 test/test_network_configuration.py diff --git a/.flake8 b/.flake8 index 5bf4bbed..1868a18d 100644 --- a/.flake8 +++ b/.flake8 @@ -30,3 +30,4 @@ per-file-ignores = camera_info.py:D101,D102,D106,D107 camera_intrinsics.py:D101,D102,D106,D107 frame_info.py:D101,D102,D106,D107 + network_configuration.py:D101,D102,D106,D107 diff --git a/.pylintrc-tests b/.pylintrc-tests index 3b62667e..c2c24417 100644 --- a/.pylintrc-tests +++ b/.pylintrc-tests @@ -6,6 +6,7 @@ disable=missing-docstring, pointless-statement, too-many-lines, too-many-statements, + too-few-public-methods, consider-using-f-string # Keep .format() as long as we want unofficial python3.5 support [FORMAT] diff --git a/continuous-integration/code-generation/datamodel_frontend_generator.py b/continuous-integration/code-generation/datamodel_frontend_generator.py index d26f8e5e..e6b52edb 100644 --- a/continuous-integration/code-generation/datamodel_frontend_generator.py +++ b/continuous-integration/code-generation/datamodel_frontend_generator.py @@ -741,6 +741,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..27f2bf1a 100644 --- a/modules/zivid/camera.py +++ b/modules/zivid/camera.py @@ -1,11 +1,16 @@ """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 +95,52 @@ 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): + """ + Apply the 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/network_configuration.py b/modules/zivid/network_configuration.py new file mode 100644 index 00000000..3b0413a2 --- /dev/null +++ b/modules/zivid/network_configuration.py @@ -0,0 +1,203 @@ +"""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)) + + @classmethod + def from_serialized(cls, value): + return _to_network_configuration( + _zivid.NetworkConfiguration.from_serialized(str(value)) + ) + + def serialize(self): + return _to_internal_network_configuration(self).serialize() + + 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..122c632f --- /dev/null +++ b/samples/sample_network_configuration.py @@ -0,0 +1,65 @@ +"""Sample demonstrating network configuration of a Zivid camera.""" +import zivid + + +def _confirm(message): + while True: + input_value = input(f"{message} [Y/n] ") + if input_value.lower() in ["y", "yes"]: + return True + if input_value.lower() in ["n", "no"]: + return False + + +def _main(): + 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 _confirm( + f"Do you want to apply the new network configuration to camera {camera.info.serial_number}?" + ): + 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}'") + + +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..65930b19 100644 --- a/src/include/ZividPython/ReleasableCamera.h +++ b/src/include/ZividPython/ReleasableCamera.h @@ -23,6 +23,10 @@ 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_data/datamodels/NetworkConfiguration.yml b/test/test_data/datamodels/NetworkConfiguration.yml new file mode 100644 index 00000000..22f04831 --- /dev/null +++ b/test/test_data/datamodels/NetworkConfiguration.yml @@ -0,0 +1,8 @@ +__version__: + serializer: 1 + data: 1 +NetworkConfiguration: + IPV4: + Address: "172.28.60.10" + Mode: manual + SubnetMask: "255.255.0.0" diff --git a/test/test_datamodel_serialize_from_serialized.py b/test/test_datamodel_serialize_from_serialized.py index d0ea469c..36a09f3c 100644 --- a/test/test_datamodel_serialize_from_serialized.py +++ b/test/test_datamodel_serialize_from_serialized.py @@ -9,6 +9,7 @@ "CameraState", "CameraInfo", "FrameInfo", + "NetworkConfiguration", "capture_assistant.SuggestSettingsParameters", ], ) diff --git a/test/test_network_configuration.py b/test/test_network_configuration.py new file mode 100644 index 00000000..030a4b6f --- /dev/null +++ b/test/test_network_configuration.py @@ -0,0 +1,77 @@ +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()