diff --git a/deapi/client.py b/deapi/client.py index 4b93ac61..ff30b1fc 100644 --- a/deapi/client.py +++ b/deapi/client.py @@ -151,7 +151,7 @@ def _initialize_attributes(self): PropertyCollection(client=self, name=collection, properties=props), ) - def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False): + def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=None): """Connect to DE-Server Parameters @@ -160,20 +160,12 @@ def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False): The host to connect to, by default "127.0.0.1" for local connection port : int, optional The port to connect to, by default 13240 - read_only : bool, optional + read_only : bool, optional (deprecated for command version >= 17) If True, the client will be in read-only mode, by default False + For command version >= 17, the read_only parameter is ignored and + the client will query the server to determine whether it should be + read-only or read-write. """ - self.read_only = read_only - if not read_only and (host == "localhost" or host == "127.0.0.1"): - tcp_no_delay = 0 # on loopback interface, nodelay causes delay - - if self.usingMmf: - self.mmf = mmap.mmap(0, MMF_DATA_BUFFER_SIZE, "ImageFileMappingObject") - self.mmf[0] = True - else: - self.usingMmf = False # Disabled MMF if connected remotely - tcp_no_delay = 1 - if logLevel == logging.DEBUG: log.debug("Connecting to server: %s", host) @@ -183,7 +175,6 @@ def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False): self.socket.connect( (host, port) ) # Connect to server reading port for sending data - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, tcp_no_delay) self.socket.setblocking(False) self.socket.settimeout(2) @@ -200,15 +191,15 @@ def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False): self.port = port log.info("Connected to server: %s, port: %d", host, port) - if cVersion >= 12: - self.set_client_read_only(read_only) - server_version = self.GetProperty("Server Software Version") server_version = re.findall(r"\d+", server_version) version = [int(part) for part in server_version[:4]] temp = version[2] + version[1] * 1000 + version[0] * 1000000 - if temp >= 2008000 and version[3] >= 12073: + if temp >= 2008003 and version[3] >= 12436: + ## version 2.8.3 build 12436+ + self.commandVersion = 17 + elif temp >= 2008000 and version[3] >= 12073: ## version 2.8.0 build 12073+ — virtual image buffer support (SDK 5.3.0) self.commandVersion = 16 elif (temp >= 2007005 and version[3] < 11274) or temp >= 2008000: @@ -234,6 +225,25 @@ def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False): self.commandVersion = 3 else: self.commandVersion = commandVersion + + if self.commandVersion >= 12 and self.commandVersion < 17: + if read_only is None: + read_only = False + self.set_client_read_only(read_only) + elif self.commandVersion >= 17: + if read_only is not None: + log.warning( + "The `read_only` argument to `connect()` is ignored for servers " + "with command version >= 17 (DE-MC version >= 2.8.3.12436). " + "The client now determines read-only status automatically." + ) + self.get_client_read_only() + + self.SelectImageTransferMode() + self.socket.setsockopt( + socket.IPPROTO_TCP, socket.TCP_NODELAY, self.tcp_no_delay + ) + log.info(f"Command Version: {self.commandVersion}") self._initialize_attributes() self.update_scan_size() @@ -245,10 +255,19 @@ def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False): def set_client_read_only(self, read_only): self.read_only = read_only - command = self._addSingleCommand(self.SET_CLIENT_READ_ONLY, None, [read_only]) + command = self._addSingleCommand( + self.SET_CLIENT_READ_ONLY_DEPRECATED, None, [read_only] + ) response = self._sendCommand(command) return response + def get_client_read_only(self): + command = self._addSingleCommand(self.GET_CLIENT_READ_ONLY, None) + response = self._sendCommand(command) + if response != False: + self.read_only = self.__getParameters(response.acknowledge[0])[0] + return response + def update_scan_size(self): self.scan_sizex = self["Scan - Size X"] self.scan_sizey = self["Scan - Size Y"] @@ -435,9 +454,11 @@ def get_property_specifications(self, property_name): The name of the property to get the specifications for """ if self.commandVersion < 13: - log.error("get_property_specifications is only supported for server version 2.7.5 and above.") + log.error( + "get_property_specifications is only supported for server version 2.7.5 and above." + ) return None - + command = self._addSingleCommand( self.GET_PROPERTY_SPECIFICATIONS, property_name ) @@ -448,9 +469,11 @@ def get_property_specifications(self, property_name): values = self.__getParameters(response.acknowledge[0]) if not values or len(values) == 0: - log.error(f"get_property_specifications({property_name}) failed, parameter size not matched.") + log.error( + f"get_property_specifications({property_name}) failed, parameter size not matched." + ) return None - + prop_spec = PropertySpecifications() param_id = 0 @@ -463,9 +486,11 @@ def get_property_specifications(self, property_name): elif data_type == "Integer": prop_spec.prop_type = PropertyType.Integer else: - log.error(f"get_property_specifications({property_name}) failed, property type not matched.") + log.error( + f"get_property_specifications({property_name}) failed, property type not matched." + ) return None - + prop_allowable_type = values[param_id] param_id += 1 @@ -477,7 +502,9 @@ def get_property_specifications(self, property_name): prop_spec.max_value = values[param_id] param_id += 1 else: - log.error(f"get_property_specifications({property_name}) failed, cannot read the min/max value.") + log.error( + f"get_property_specifications({property_name}) failed, cannot read the min/max value." + ) return None elif prop_allowable_type == "Set": prop_spec.prop_allowable_type = PropertyAllowableType.Set @@ -485,7 +512,9 @@ def get_property_specifications(self, property_name): elif prop_allowable_type == "AllowAll": prop_spec.prop_allowable_type = PropertyAllowableType.AllowAll else: - log.error(f"get_property_specifications({property_name}) failed, unknown allowable type.") + log.error( + f"get_property_specifications({property_name}) failed, unknown allowable type." + ) return None prop_spec.category = values[-4] @@ -2823,6 +2852,19 @@ def __del__(self): if self.connected: self.disconnect() + def SelectImageTransferMode(self): + if not self.read_only and ( + self.host == "localhost" or self.host == "127.0.0.1" + ): + self.tcp_no_delay = 0 # on loopback interface, nodelay causes delay + + if self.usingMmf: + self.mmf = mmap.mmap(0, MMF_DATA_BUFFER_SIZE, "ImageFileMappingObject") + self.mmf[0] = True + else: + self.usingMmf = False # Disabled MMF if connected remotely + self.tcp_no_delay = 1 + # get multiple parameters from a single acknowledge packet def __getParameters(self, single_acknowledge=None): output = [] @@ -3162,6 +3204,7 @@ def ParseChangedProperties(self, changedProperties, response): height = 0 mmf = 0 usingMmf = True + tcp_no_delay = 0 debugImagesFolder = "D:\\DebugImages\\" connected = False camera = "" @@ -3201,7 +3244,7 @@ def ParseChangedProperties(self, changedProperties, response): SET_SCAN_ROI = 28 SET_SCAN_SIZE_AND_GET_CHANGED_PROPERTIES = 29 SET_SCAN_ROI__AND_GET_CHANGED_PROPERTIES = 30 - SET_CLIENT_READ_ONLY = 31 + SET_CLIENT_READ_ONLY_DEPRECATED = 31 SET_SCAN_XY_ARRAY = 32 SET_ADAPTIVE_ROI = 33 SET_ADAPTIVE_ROI_AND_GET_CHANGED_PROPERTIES = 34 @@ -3213,6 +3256,7 @@ def ParseChangedProperties(self, changedProperties, response): LIST_REGISTERS = 40 GET_VIRTUAL_IMAGE_INFO = 41 GET_VIRTUAL_IMAGE = 42 + GET_CLIENT_READ_ONLY = 43 MMF_DATA_HEADER_SIZE = 24 diff --git a/deapi/data_types.py b/deapi/data_types.py index 46cdf2fa..6633ba7b 100644 --- a/deapi/data_types.py +++ b/deapi/data_types.py @@ -665,8 +665,8 @@ def __init__( prop_type = None # Undef | String | Float | Integer prop_allowable_type = None # Range | Set | AllowAll - min_value = None # Minimum value for Range type - max_value = None # Maximum value for Range type + min_value = None # Minimum value for Range type + max_value = None # Maximum value for Range type values = None # List of values for Set allowable type category = None # "Alias" | "Advanced" | "Basic" | "Deprecated" | "Engineering" | Obsolete" default_value = None # Default value diff --git a/deapi/simulated_server/fake_server.py b/deapi/simulated_server/fake_server.py index 693e75f6..56e342dc 100644 --- a/deapi/simulated_server/fake_server.py +++ b/deapi/simulated_server/fake_server.py @@ -151,6 +151,7 @@ def __init__(self, dataset="grains", socket=None): self.fake_data = None self.socket = socket self.current_movie_index = 0 + self.is_read_only = False with open(inp_file) as f: values = json.load(f) @@ -309,9 +310,14 @@ def _respond_to_command(self, command=None): return self._fake_list_cameras(command) elif ( command.command[0].command_id - == self.SET_CLIENT_READ_ONLY + commandVersion * 100 + == self.SET_CLIENT_READ_ONLY_DEPRECATED + commandVersion * 100 ): return self._fake_set_client_read_only(command) + elif ( + command.command[0].command_id + == self.GET_CLIENT_READ_ONLY + commandVersion * 100 + ): + return self._fake_get_client_read_only(command) elif ( command.command[0].command_id == self.SET_VIRTUAL_MASK + commandVersion * 100 @@ -343,6 +349,17 @@ def _fake_set_client_read_only(self, command): ack1.command_id = command.command[0].command_id return (acknowledge_return,) + def _fake_get_client_read_only(self, command): + acknowledge_return = pb.DEPacket() + acknowledge_return.type = pb.DEPacket.P_ACKNOWLEDGE + ack1 = acknowledge_return.acknowledge.add() + ack1.command_id = command.command[0].command_id + bool_param = ack1.parameter.add() + bool_param.type = pb.AnyParameter.P_BOOL + bool_param.p_bool = False + self.is_read_only = bool_param.p_bool + return (acknowledge_return,) + def _fake_set_virtual_mask(self, command): acknowledge_return = pb.DEPacket() acknowledge_return.type = pb.DEPacket.P_ACKNOWLEDGE @@ -919,6 +936,7 @@ def _fake_get_virtual_image(self, command): SET_SCAN_ROI = 28 SET_SCAN_SIZE_AND_GET_CHANGED_PROPERTIES = 29 SET_SCAN_ROI__AND_GET_CHANGED_PROPERTIES = 30 - SET_CLIENT_READ_ONLY = 31 + SET_CLIENT_READ_ONLY_DEPRECATED = 31 GET_VIRTUAL_IMAGE_INFO = 41 GET_VIRTUAL_IMAGE = 42 + GET_CLIENT_READ_ONLY = 43 diff --git a/deapi/tests/test_client.py b/deapi/tests/test_client.py index 5c585dac..bad9fb80 100644 --- a/deapi/tests/test_client.py +++ b/deapi/tests/test_client.py @@ -179,17 +179,24 @@ def test_property_specifications_set(self, client, bin_sw): sp = client.get_property_specifications("Binning Y") assert isinstance(sp, PropertySpecifications) assert sp.current_value == str(bin_sw) - assert ( - sp.values - == ['1', '2', '4', '8', '16', '32', '64', '128', '256', '512', '1024'] - ) + assert sp.values == [ + "1", + "2", + "4", + "8", + "16", + "32", + "64", + "128", + "256", + "512", + "1024", + ] client.set_property("Hardware Binning X", 2) client.set_property("Hardware Binning Y", 2) sp = client.get_property_specifications("Binning Y") assert sp.current_value == str(bin_sw) - assert ( - sp.values == ['1', '2', '4', '8', '16', '32', '64', '128', '256', '512'] - ) + assert sp.values == ["1", "2", "4", "8", "16", "32", "64", "128", "256", "512"] @pytest.mark.parametrize("bin", [1, 2]) @pytest.mark.parametrize("offsetx", [0, 512]) diff --git a/deapi/tests/test_scanning/test_virtual_image_buffers.py b/deapi/tests/test_scanning/test_virtual_image_buffers.py index 55b03dcd..fedfede4 100644 --- a/deapi/tests/test_scanning/test_virtual_image_buffers.py +++ b/deapi/tests/test_scanning/test_virtual_image_buffers.py @@ -91,8 +91,8 @@ def test_streaming_all_virtual_buffers(self, client): while not finished: for buf_id in range(NUM_VIRTUAL_BUFFERS): - status, frame_index, pattern_index, image = client.get_virtual_image_buffer( - buf_id, virtual_image_info=info + status, frame_index, pattern_index, image = ( + client.get_virtual_image_buffer(buf_id, virtual_image_info=info) ) print( f"buf_id={buf_id} frame={frame_index} pattern={pattern_index} status={status} image shape: {image.shape if image is not None else None}" @@ -164,8 +164,8 @@ def test_streaming_all_virtual_buffers_one_off(self, client): while not finished: for buf_id in range(NUM_VIRTUAL_BUFFERS): - status, frame_index, pattern_index, image = client.get_virtual_image_buffer( - buf_id, virtual_image_info=info + status, frame_index, pattern_index, image = ( + client.get_virtual_image_buffer(buf_id, virtual_image_info=info) ) print( f"buf_id={buf_id} frame={frame_index} pattern={pattern_index} status={status} image shape: {image.shape if image is not None else None}" @@ -253,8 +253,10 @@ def test_streaming_multiple_xy_arrays(self, client): for buf_id in range(NUM_VIRTUAL_BUFFERS): got_frame = False while not got_frame: - status, frame_index, pattern_index, image = client.get_virtual_image_buffer( - buf_id, virtual_image_info=info, timeout_msec=1000 # 1 sec + status, frame_index, pattern_index, image = ( + client.get_virtual_image_buffer( + buf_id, virtual_image_info=info, timeout_msec=1000 # 1 sec + ) ) if status == MovieBufferStatus.OK: received_frames.append( diff --git a/deapi/version.py b/deapi/version.py index 82aad067..7d39d4aa 100644 --- a/deapi/version.py +++ b/deapi/version.py @@ -1,11 +1,12 @@ version = "5.3.0" versionInfo = list(map(int, version.split("."))) -commandVersion = 16 +commandVersion = 17 # Maps each commandVersion to a representative server software version string. # Used by FakeServer so its reported "Server Software Version" always matches # the commandVersion used for dispatch, making tests version-agnostic. _command_version_to_server_version = { + 17: "2.8.3.12436", 16: "2.8.0.12073", 15: "2.7.5.1000", 13: "2.7.4.10590", @@ -18,5 +19,5 @@ # The server version string that corresponds to the current commandVersion. fake_server_software_version = _command_version_to_server_version.get( - commandVersion, "2.8.0.12073" + commandVersion, "2.8.3.12436" )