From 6724cd23dbbf094769e4ecf1d94838d63167b5da Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 01/38] Add `StageWidget` for each stage control --- iquip/apps/stage.py | 85 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 6c8bbe12..77f08919 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -170,3 +170,88 @@ def __getattr__(self, name: str) -> Callable: """ signal = getattr(self.manager, name) return functools.partial(signal.emit, self.key) + + +class StageWidget(QWidget): + """UI for stage control. + + Signals: + moveTo(position_m): Absolute move button is clicked, with the destination + position in meters. + moveBy(displacement_m): Relative move button is clicked, with the desired + displacement in meters. + + All the displayed values are in mm unit. + However, the values for interface (methods and signals) are in m. + """ + + moveTo = pyqtSignal(float) + moveBy = pyqtSignal(float) + + def __init__(self, parent: Optional[QWidget] = None): + """Extended.""" + super().__init__(parent=parent) + # widgets + self.positionBox = QDoubleSpinBox(self) + self.positionBox.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.positionBox.setReadOnly(True) + self.positionBox.setDecimals(3) + self.positionBox.setSuffix("mm") + self.absoluteBox = QDoubleSpinBox(self) + self.absoluteBox.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.absoluteBox.setDecimals(3) + self.absoluteBox.setSingleStep(0.001) + self.absoluteButton = QPushButton("Go", self) + self.relativeBox = QDoubleSpinBox(self) + self.relativeBox.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.relativeBox.setDecimals(3) + self.relativeBox.setSingleStep(0.001) + self.relativePositiveButton = QPushButton("Move +", self) + self.relativeNegativeButton = QPushButton("Move -", self) + # layout + abosluteLayout = QVBoxLayout() + abosluteLayout.addWidget(self.absoluteBox) + abosluteLayout.addWidget(self.absoluteButton) + relativeLayout = QVBoxLayout() + relativeLayout.addWidget(self.relativePositiveButton) + relativeLayout.addWidget(self.relativeBox) + relativeLayout.addWidget(self.relativeNegativeButton) + moveLayout = QHBoxLayout() + moveLayout.addLayout(abosluteLayout) + moveLayout.addLayout(relativeLayout) + layout = QVBoxLayout(self) + layout.addWidget(self.positionBox) + layout.addLayout(moveLayout) + # signal connection + self.absoluteButton.clicked.connect(self._absoluteMove) + self.relativePositiveButton.clicked.connect(self._relativePositiveMove) + self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) + + @pyqtSlot(float) + def setPosition(self, position_m: float): + """Sets the current position displayed on the widget. + + Args: + position_m: Position in meters. + """ + self.positionBox.setValue(position_m * 1e3) + + def position(self) -> float: + """Returns the current position in meters.""" + return self.positionBox.value() / 1e3 + + @pyqtSlot() + def _absoluteMove(self): + """Absolute move button is clicked.""" + self.moveTo.emit(self.absoluteBox.value() / 1e3) + + @pyqtSlot() + def _relativePositiveMove(self): + """Relative positive move button is clicked.""" + self.moveBy.emit(self.relativeBox.value() / 1e3) + + @pyqtSlot() + def _relativeNegativeMove(self): + """Relative negative move button is clicked.""" + self.moveBy.emit(-self.relativeBox.value() / 1e3) + From decdfa95d5c9a1f14efd43b8a50f4d6a31d3d188 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 02/38] Set alignment of spin boxes --- iquip/apps/stage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 77f08919..3b319de4 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -197,15 +197,18 @@ def __init__(self, parent: Optional[QWidget] = None): self.positionBox.setReadOnly(True) self.positionBox.setDecimals(3) self.positionBox.setSuffix("mm") + self.positionBox.setAlignment(Qt.AlignHCenter) self.absoluteBox = QDoubleSpinBox(self) self.absoluteBox.setButtonSymbols(QAbstractSpinBox.NoButtons) self.absoluteBox.setDecimals(3) self.absoluteBox.setSingleStep(0.001) + self.absoluteBox.setAlignment(Qt.AlignRight) self.absoluteButton = QPushButton("Go", self) self.relativeBox = QDoubleSpinBox(self) self.relativeBox.setButtonSymbols(QAbstractSpinBox.NoButtons) self.relativeBox.setDecimals(3) self.relativeBox.setSingleStep(0.001) + self.relativeBox.setAlignment(Qt.AlignRight) self.relativePositiveButton = QPushButton("Move +", self) self.relativeNegativeButton = QPushButton("Move -", self) # layout From 98d054f7394bf3abeca2e31aa65a418f261561bc Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 03/38] Add `StageControllerFrame` --- iquip/apps/stage.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 3b319de4..ff266d99 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -258,3 +258,32 @@ def _relativeNegativeMove(self): """Relative negative move button is clicked.""" self.moveBy.emit(-self.relativeBox.value() / 1e3) + +class StageControllerFrame(QWidget): + """Frame for StageControllerApp. + + Attributes: + widgets: Dictionary whose keys are stage names and the values are the + corresponding stage widgets. + """ + + def __init__( + self, + stages: Dict[str, Dict[str, Any]], + parent: Optional[QWidget] = None, + ): + """Extended. + + Args: + See StageControllerApp. + """ + super().__init__(parent=parent) + self.widgets: Dict[str, StageWidget] = {} + layout = QGridLayout(self) + for stage_name, stage_info in stages.items(): + widget = StageWidget(self) + groupbox = QGroupBox(stage_name, self) + groupboxLayout = QHBoxLayout(groupbox) + groupboxLayout.addWidget(widget) + layout.addWidget(groupbox, *stage_info["index"]) + self.widgets[stage_name] = widget From fd1173105a6769cf80e121ee18b84632cea59c68 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 04/38] Add a connect button in `StageWidget` --- iquip/apps/stage.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index ff266d99..72443212 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -180,6 +180,7 @@ class StageWidget(QWidget): position in meters. moveBy(displacement_m): Relative move button is clicked, with the desired displacement in meters. + tryConnect(): Connect button is clicked. All the displayed values are in mm unit. However, the values for interface (methods and signals) are in m. @@ -187,11 +188,13 @@ class StageWidget(QWidget): moveTo = pyqtSignal(float) moveBy = pyqtSignal(float) + tryConnect = pyqtSignal() def __init__(self, parent: Optional[QWidget] = None): """Extended.""" super().__init__(parent=parent) # widgets + self.connectButton = QPushButton("Connect", self) self.positionBox = QDoubleSpinBox(self) self.positionBox.setButtonSymbols(QAbstractSpinBox.NoButtons) self.positionBox.setReadOnly(True) @@ -211,6 +214,7 @@ def __init__(self, parent: Optional[QWidget] = None): self.relativeBox.setAlignment(Qt.AlignRight) self.relativePositiveButton = QPushButton("Move +", self) self.relativeNegativeButton = QPushButton("Move -", self) + self._inner = QWidget(self) # except connectButton # layout abosluteLayout = QVBoxLayout() abosluteLayout.addWidget(self.absoluteBox) @@ -222,14 +226,31 @@ def __init__(self, parent: Optional[QWidget] = None): moveLayout = QHBoxLayout() moveLayout.addLayout(abosluteLayout) moveLayout.addLayout(relativeLayout) + innerLayout = QVBoxLayout(self._inner) + innerLayout.addWidget(self.positionBox) + innerLayout.addLayout(moveLayout) layout = QVBoxLayout(self) - layout.addWidget(self.positionBox) - layout.addLayout(moveLayout) + layout.addWidget(self.connectButton) + layout.addWidget(self._inner) # signal connection + self.connectButton.clicked.connect(self.tryConnect) self.absoluteButton.clicked.connect(self._absoluteMove) self.relativePositiveButton.clicked.connect(self._relativePositiveMove) self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) + @pyqtSlot(bool) + def setConnected(self, connected: bool): + """Sets the current connection status. + + This also changes the enabled status and the connect button text. + + Args: + connected: True for connected, False for disconnected. + """ + self._inner.setEnabled(connected) + self.connectButton.setEnabled(not connected) + self.connectButton.setText("Connected" if connected else "Connect") + @pyqtSlot(float) def setPosition(self, position_m: float): """Sets the current position displayed on the widget. From 83ee8cfaaa36dfed95f89155a2a6973af93931fa Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 05/38] Initialize the state of `StageWidget` --- iquip/apps/stage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 72443212..1661445c 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -237,6 +237,8 @@ def __init__(self, parent: Optional[QWidget] = None): self.absoluteButton.clicked.connect(self._absoluteMove) self.relativePositiveButton.clicked.connect(self._relativePositiveMove) self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) + # initialize state + self.setConnected(False) @pyqtSlot(bool) def setConnected(self, connected: bool): From 735063b88c48fccb089a96ec0fd192dc5978eecb Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 06/38] Add `connectionChanged` and `exception` signals --- iquip/apps/stage.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 1661445c..264a1aa2 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -57,9 +57,17 @@ class StageManager(QObject): Instead, use signals to communicate. Signals: - See _signal() method for each signal. + connectionChanged(key, connected): A client connection status is changed, + with its string key and connection status as True for connected, False + for disconnected. + exception(key, exception): An exception is occurred with the corresponding + client key and the exception object. + See _signal() method for the other signals. """ + connectionChanged = pyqtSignal(str, bool) + exception = pyqtSignal(str, Exception) + clear = pyqtSignal() closeTarget = pyqtSignal(str) connectTarget = pyqtSignal(str, tuple) From 19c44f009454e10175160b603eae9dc223232430 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 07/38] Emit `connectionChanged` signal when connected --- iquip/apps/stage.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 264a1aa2..2206be01 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -123,7 +123,12 @@ def _connectTarget(self, key: str, info: RPCTargetInfo): client = self._clients.get(key, None) if client is not None: client.close_rpc() - self._clients[key] = Client(*info) + try: + self._clients[key] = Client(*info) + except OSError as error: + self.exception.emit(key, error) + else: + self.connectionChanged.emit(key, True) @pyqtSlot(str, float) @use_client From 69ac51cb07b8bf7223b3ba3c5a2d42d995249f49 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 08/38] Rename `execption` -> `clientError` --- iquip/apps/stage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 2206be01..a9956a81 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -60,13 +60,13 @@ class StageManager(QObject): connectionChanged(key, connected): A client connection status is changed, with its string key and connection status as True for connected, False for disconnected. - exception(key, exception): An exception is occurred with the corresponding - client key and the exception object. + clientError(key, exception): An exception is occurred during client operation, + with the corresponding client key and exception object. See _signal() method for the other signals. """ connectionChanged = pyqtSignal(str, bool) - exception = pyqtSignal(str, Exception) + clientError = pyqtSignal(str, Exception) clear = pyqtSignal() closeTarget = pyqtSignal(str) @@ -126,7 +126,7 @@ def _connectTarget(self, key: str, info: RPCTargetInfo): try: self._clients[key] = Client(*info) except OSError as error: - self.exception.emit(key, error) + self.clientError.emit(key, error) else: self.connectionChanged.emit(key, True) From eddecf9a92a6126db4b851d816b02a1c752556c3 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 09/38] Reimplement `clear()` to use `closeTarget()` --- iquip/apps/stage.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index a9956a81..0a2320f3 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -93,9 +93,8 @@ def __init__(self, parent: Optional[QObject] = None): @pyqtSlot() def _clear(self): """Closes all the RPC clients and clears the client dictionary.""" - for client in self._clients.values(): - client.close_rpc() - self._clients.clear() + for key in tuple(self._clients): + self._closeTarget(key) @pyqtSlot(str) def _closeTarget(self, key: str): From fb60d77d263622ddf4717e9ee49498af14d69ddf Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 10/38] Emit `connectionChanged` signal in `closeTarget()` --- iquip/apps/stage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 0a2320f3..a7a7e6c8 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -109,6 +109,7 @@ def _closeTarget(self, key: str): logger.error("Failed to close target: RPC client %s does not exist.", key) return client.close_rpc() + self.connectionChanged.emit(key, False) @pyqtSlot(str, tuple) def _connectTarget(self, key: str, info: RPCTargetInfo): From 5968aea676bca5d2160cefb105abbdb9ca3fc226 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:06:17 +0900 Subject: [PATCH 11/38] Use `closeTarget()` in `connectTarget()` --- iquip/apps/stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index a7a7e6c8..6c713315 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -122,7 +122,7 @@ def _connectTarget(self, key: str, info: RPCTargetInfo): """ client = self._clients.get(key, None) if client is not None: - client.close_rpc() + self._closeTarget(key) try: self._clients[key] = Client(*info) except OSError as error: From 8f16352b4fa354c1100e371b9185e917610cc6cb Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:09:18 +0900 Subject: [PATCH 12/38] Emit signals in `use_client` decorator --- iquip/apps/stage.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 6c713315..593181b0 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -34,17 +34,18 @@ def wrapped(self: StageManager, key: str, *args, **kwargs): client = self._clients.get(key, None) # pylint: disable=protected-access if client is None: logger.error("Failed to get client %s.", key) + self.clientError.emit(key, KeyError(f"There is no client {key}")) return try: function(self, client, *args, **kwargs) - except (AttributeError, OSError, ValueError): + except (AttributeError, OSError, ValueError) as error: logger.exception( "Error occurred while running %s with client %s.", function.__name__, key, ) - client.close_rpc() - self._clients.pop(key) # pylint: disable=protected-access + self.clientError.emit(key, error) + self._closeTarget(key) return wrapped From f5fe2e3f1fcfe6e60e9fea54d15a2cc6ab691b3b Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:09:18 +0900 Subject: [PATCH 13/38] Remove redundant docstring for `client` argument --- iquip/apps/stage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 593181b0..6c459e53 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -137,7 +137,6 @@ def _moveBy(self, client: Client, displacement_m: float): """Moves the stage by given displacement. Args: - client: Client object. displacement_m: Relative move displacement in meters. """ client.move_by(displacement_m) @@ -148,7 +147,6 @@ def _moveTo(self, client: Client, position_m: float): """Moves the stage to given position. Args: - client: Client object. position_m: Absolute destination position in meters. """ client.move_to(position_m) From 3a69e8d872f8fdb18700626a207c77857ad74023 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 14/38] Don't remove `key` in `use_client` --- iquip/apps/stage.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 6c459e53..54891a3e 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -14,15 +14,15 @@ RPCTargetInfo = Tuple[str, int, str] # ip, port, target_name def use_client(function: Callable[..., None]) -> Callable[..., None]: - """Decorator which substitutes a string key to a client object. + """Decorator which adds a client object in arguments. If the key does not exist, function is not called at all. If an OSError occurs while running function, the RPC client is closed and removed from the client dictionary. Args: - function: Decorated function. It should take a Client object as the - first argument. + function: Decorated function. It should take a Client object and its key + as the first and second arguments, respectively. """ @functools.wraps(function) def wrapped(self: StageManager, key: str, *args, **kwargs): @@ -37,7 +37,7 @@ def wrapped(self: StageManager, key: str, *args, **kwargs): self.clientError.emit(key, KeyError(f"There is no client {key}")) return try: - function(self, client, *args, **kwargs) + function(self, client, key, *args, **kwargs) except (AttributeError, OSError, ValueError) as error: logger.exception( "Error occurred while running %s with client %s.", @@ -133,7 +133,7 @@ def _connectTarget(self, key: str, info: RPCTargetInfo): @pyqtSlot(str, float) @use_client - def _moveBy(self, client: Client, displacement_m: float): + def _moveBy(self, client: Client, _key: str, displacement_m: float): """Moves the stage by given displacement. Args: @@ -143,7 +143,7 @@ def _moveBy(self, client: Client, displacement_m: float): @pyqtSlot(str, float) @use_client - def _moveTo(self, client: Client, position_m: float): + def _moveTo(self, client: Client, _key: str, position_m: float): """Moves the stage to given position. Args: From 5987fab3d9f776cb831e8160aee293bf6f60a293 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 15/38] Add position requester and reporter --- iquip/apps/stage.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 54891a3e..6bfa45d2 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -63,15 +63,19 @@ class StageManager(QObject): for disconnected. clientError(key, exception): An exception is occurred during client operation, with the corresponding client key and exception object. - See _signal() method for the other signals. + positionReported(key, position_m): The current position of a stage is + reported, with the client key and the position in meters. + See _signal() method for the other signal's. """ connectionChanged = pyqtSignal(str, bool) clientError = pyqtSignal(str, Exception) + positionReported = pyqtSignal(str, float) clear = pyqtSignal() closeTarget = pyqtSignal(str) connectTarget = pyqtSignal(str, tuple) + getPosition = pyqtSignal(str) moveBy = pyqtSignal(str, float) moveTo = pyqtSignal(str, float) @@ -131,6 +135,12 @@ def _connectTarget(self, key: str, info: RPCTargetInfo): else: self.connectionChanged.emit(key, True) + @pyqtSlot(str) + @use_client + def _getPosition(self, client: Client, key: str): + """Requests the current stage position in meters and reports it.""" + self.positionReported.emit(key, client.get_position()) + @pyqtSlot(str, float) @use_client def _moveBy(self, client: Client, _key: str, displacement_m: float): From 7efcdcc4acc02929d10b87fb8dc5d0041933ca62 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 16/38] Add `isConnected()` --- iquip/apps/stage.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 6bfa45d2..beaab0b3 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -275,6 +275,10 @@ def setConnected(self, connected: bool): self.connectButton.setEnabled(not connected) self.connectButton.setText("Connected" if connected else "Connect") + def isConnected(self) -> bool: + """Returns whether the client is currently connected.""" + return self._inner.isEnabled() + @pyqtSlot(float) def setPosition(self, position_m: float): """Sets the current position displayed on the widget. From d0485e525c9d738bf36d3755e4dbb51ea10c98a3 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 17/38] Add `StageControllerApp` --- iquip/apps/stage.py | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index beaab0b3..50c49b5a 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -336,3 +336,53 @@ def __init__( groupboxLayout.addWidget(widget) layout.addWidget(groupbox, *stage_info["index"]) self.widgets[stage_name] = widget + + +class StageControllerApp(qiwis.BaseApp): + """App for monitoring and controlling motorized stages.""" + + def __init__( + self, + name: str, + stages: Dict[str, Dict[str, Any]], + parent: Optional[QObject] = None, + ): + """Extended. + + Args: + stages: Dictionary of stage information. Each key is the name of the + stage and the value is agian a dictionary, whose structure is: + { + "index": [row, column], + "target": ["ip", port, "target_name"] + } + """ + super().__init__(name, parent=parent) + # setup threaded manager + self.thread = QThread() + self.manager = StageManager() + self.proxies = {key: StageProxy(self.manager, key) for key in stages} + self.manager.moveToThread(self.thread) + self.thread.finished.connect(self.manager.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + self.thread.start() + # timer for periodic position read + self.timer = QTimer(self) + self.timer.start(500) + # setup controller frame + self.frame = StageControllerFrame(stages, self) + for key, info in stages.items(): + proxy = self.proxies[key] + widget = self.frame.widgets[key] + widget.tryConnect.connect(functools.partial(proxy.connectTarget, info["target"])) + widget.moveBy.connect(proxy.moveBy) + widget.moveTo.connect(proxy.moveTo) + + + def __del__(self): + """Quits the thread before destructing.""" + self.thread.quit() + + def frames(self) -> Tuple[Tuple[str, StageControllerFrame]]: + """Overridden.""" + return (("", self.frame),) From ed4bf11833308cc95d9652ac65229e791b336682 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 18/38] Connect `timeout` signal to read stage positions --- iquip/apps/stage.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 50c49b5a..0c99fde7 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -369,6 +369,7 @@ def __init__( # timer for periodic position read self.timer = QTimer(self) self.timer.start(500) + self.timer.timeout.connect(self.readAllPositions) # setup controller frame self.frame = StageControllerFrame(stages, self) for key, info in stages.items(): @@ -378,6 +379,12 @@ def __init__( widget.moveBy.connect(proxy.moveBy) widget.moveTo.connect(proxy.moveTo) + @pyqtSlot() + def readAllPositions(self): + """Requests positions of all connected stages.""" + for key, widget in self.frame.widgets.items(): + if widget.isConnected(): + self.proxies[key].getPosition() def __del__(self): """Quits the thread before destructing.""" From 4f13db28482ccdac742102b2b428866d0b783344 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 19/38] Connect `connectionChanged` signal --- iquip/apps/stage.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 0c99fde7..c1994961 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -369,7 +369,6 @@ def __init__( # timer for periodic position read self.timer = QTimer(self) self.timer.start(500) - self.timer.timeout.connect(self.readAllPositions) # setup controller frame self.frame = StageControllerFrame(stages, self) for key, info in stages.items(): @@ -378,6 +377,11 @@ def __init__( widget.tryConnect.connect(functools.partial(proxy.connectTarget, info["target"])) widget.moveBy.connect(proxy.moveBy) widget.moveTo.connect(proxy.moveTo) + # signal connection + self.timer.timeout.connect(self.readAllPositions, type=Qt.QueuedConnection) + self.manager.connectionChanged.connect( + self.handleConnectionChanged, type=Qt.QueuedConnection + ) @pyqtSlot() def readAllPositions(self): @@ -386,6 +390,20 @@ def readAllPositions(self): if widget.isConnected(): self.proxies[key].getPosition() + @pyqtSlot(str, bool) + def handleConnectionChanged(self, key: str, connected: bool): + """Handles connectionChanged signal. + + Args: + See StageManager.connectionChanged signal. + """ + try: + widget = self.frame.widgets[key] + except KeyError: + logger.exception("Connection changed key does not exist.") + else: + widget.setConnected(connected) + def __del__(self): """Quits the thread before destructing.""" self.thread.quit() From 2980825d5a9a717bb8d712afc4031a99c0fe053c Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 20/38] Handles `clientError` signal --- iquip/apps/stage.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index c1994961..fe9c5021 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -404,6 +404,15 @@ def handleConnectionChanged(self, key: str, connected: bool): else: widget.setConnected(connected) + @pyqtSlot(str, Exception) + def handleClientError(self, key: str, _error: Exception): + """Handles clientError signal. + + Args: + See StageManager.clientError signal. + """ + self.handleConnectionChanged(key, False) + def __del__(self): """Quits the thread before destructing.""" self.thread.quit() From 0de7d92803a1e5ac8511cc5662dc10b5a9a83c2e Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 21/38] Connect slots for `clientError` signal --- iquip/apps/stage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index fe9c5021..b9243fda 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -382,6 +382,9 @@ def __init__( self.manager.connectionChanged.connect( self.handleConnectionChanged, type=Qt.QueuedConnection ) + self.manager.clientError.connect( + self.handleClientError, type=Qt.QueuedConnection + ) @pyqtSlot() def readAllPositions(self): From 58a52502228cf1507ff1c2caa991dec00096da76 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 22/38] Handle `positionReported` signal --- iquip/apps/stage.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index b9243fda..3650dada 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -385,6 +385,9 @@ def __init__( self.manager.clientError.connect( self.handleClientError, type=Qt.QueuedConnection ) + self.manager.positionReported.connect( + self.handlePositionReported, type=Qt.QueuedConnection + ) @pyqtSlot() def readAllPositions(self): @@ -416,6 +419,20 @@ def handleClientError(self, key: str, _error: Exception): """ self.handleConnectionChanged(key, False) + @pyqtSlot(str, float) + def handlePositionReported(self, key: str, position_m: float): + """Handles positionReported signal. + + Args: + See StageManager.positionReported signal. + """ + try: + widget = self.frame.widgets[key] + except KeyError: + logger.exception("Position reported key does not exist.") + else: + widget.setPosition(position_m) + def __del__(self): """Quits the thread before destructing.""" self.thread.quit() From 7d99c82688a151d65607129110f67c4644f51c2e Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 23/38] Import --- iquip/apps/stage.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 3650dada..0a955cf5 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -4,10 +4,16 @@ import functools import logging -from typing import Callable, Dict, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple from sipyco.pc_rpc import Client -from PyQt5.QtCore import QObject, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtWidgets import ( + QAbstractSpinBox, QDoubleSpinBox, QGroupBox, QPushButton, QWidget, + QVBoxLayout, QHBoxLayout, QGridLayout, +) + +import qiwis logger = logging.getLogger(__name__) From de9cc11a98fadac7217d1705e94800b6546d795c Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 24/38] Remove trailing whitespaces --- iquip/apps/stage.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 0a955cf5..cc8ea4ee 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -267,7 +267,7 @@ def __init__(self, parent: Optional[QWidget] = None): self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) # initialize state self.setConnected(False) - + @pyqtSlot(bool) def setConnected(self, connected: bool): """Sets the current connection status. @@ -297,17 +297,17 @@ def setPosition(self, position_m: float): def position(self) -> float: """Returns the current position in meters.""" return self.positionBox.value() / 1e3 - + @pyqtSlot() def _absoluteMove(self): """Absolute move button is clicked.""" self.moveTo.emit(self.absoluteBox.value() / 1e3) - + @pyqtSlot() def _relativePositiveMove(self): """Relative positive move button is clicked.""" self.moveBy.emit(self.relativeBox.value() / 1e3) - + @pyqtSlot() def _relativeNegativeMove(self): """Relative negative move button is clicked.""" @@ -442,7 +442,7 @@ def handlePositionReported(self, key: str, position_m: float): def __del__(self): """Quits the thread before destructing.""" self.thread.quit() - + def frames(self) -> Tuple[Tuple[str, StageControllerFrame]]: """Overridden.""" return (("", self.frame),) From d1caedc953d8d83dbdcedbfaf3fba25821c92a03 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 25/38] Add pylint disable comments --- iquip/apps/stage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index cc8ea4ee..17d56cdd 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -51,7 +51,7 @@ def wrapped(self: StageManager, key: str, *args, **kwargs): key, ) self.clientError.emit(key, error) - self._closeTarget(key) + self._closeTarget(key) # pylint: disable=protected-access return wrapped @@ -200,7 +200,7 @@ def __getattr__(self, name: str) -> Callable: return functools.partial(signal.emit, self.key) -class StageWidget(QWidget): +class StageWidget(QWidget): # pylint: disable=too-many-instance-attributes """UI for stage control. Signals: From 8c7578cebf58423a8c0ea774be2c47ac3ac9bdbe Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 26/38] Bugfix --- iquip/apps/stage.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 17d56cdd..12340b42 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -93,6 +93,7 @@ def __init__(self, parent: Optional[QObject] = None): "clear", "closeTarget", "connectTarget", + "getPosition", "moveBy", "moveTo", ) @@ -376,11 +377,11 @@ def __init__( self.timer = QTimer(self) self.timer.start(500) # setup controller frame - self.frame = StageControllerFrame(stages, self) + self.frame = StageControllerFrame(stages) for key, info in stages.items(): proxy = self.proxies[key] widget = self.frame.widgets[key] - widget.tryConnect.connect(functools.partial(proxy.connectTarget, info["target"])) + widget.tryConnect.connect(functools.partial(proxy.connectTarget, tuple(info["target"]))) widget.moveBy.connect(proxy.moveBy) widget.moveTo.connect(proxy.moveTo) # signal connection From 67149aeb4f6f4f6d15b9bad295aafe170af1df54 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:10:05 +0900 Subject: [PATCH 27/38] Add timeout --- iquip/apps/stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 12340b42..5544f357 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -136,7 +136,7 @@ def _connectTarget(self, key: str, info: RPCTargetInfo): if client is not None: self._closeTarget(key) try: - self._clients[key] = Client(*info) + self._clients[key] = Client(*info, timeout=5) except OSError as error: self.clientError.emit(key, error) else: From c9c2e941be67222913296142b1ba7ea950ea5c7e Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:50:13 +0900 Subject: [PATCH 28/38] Reduce the diff for smaller PR --- iquip/apps/stage.py | 144 ++------------------------------------------ 1 file changed, 4 insertions(+), 140 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 5544f357..76349b3a 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -4,17 +4,15 @@ import functools import logging -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Callable, Dict, Optional, Tuple from sipyco.pc_rpc import Client -from PyQt5.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QObject, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QAbstractSpinBox, QDoubleSpinBox, QGroupBox, QPushButton, QWidget, - QVBoxLayout, QHBoxLayout, QGridLayout, + QAbstractSpinBox, QDoubleSpinBox, QPushButton, QWidget, + QVBoxLayout, QHBoxLayout, ) -import qiwis - logger = logging.getLogger(__name__) RPCTargetInfo = Tuple[str, int, str] # ip, port, target_name @@ -313,137 +311,3 @@ def _relativePositiveMove(self): def _relativeNegativeMove(self): """Relative negative move button is clicked.""" self.moveBy.emit(-self.relativeBox.value() / 1e3) - - -class StageControllerFrame(QWidget): - """Frame for StageControllerApp. - - Attributes: - widgets: Dictionary whose keys are stage names and the values are the - corresponding stage widgets. - """ - - def __init__( - self, - stages: Dict[str, Dict[str, Any]], - parent: Optional[QWidget] = None, - ): - """Extended. - - Args: - See StageControllerApp. - """ - super().__init__(parent=parent) - self.widgets: Dict[str, StageWidget] = {} - layout = QGridLayout(self) - for stage_name, stage_info in stages.items(): - widget = StageWidget(self) - groupbox = QGroupBox(stage_name, self) - groupboxLayout = QHBoxLayout(groupbox) - groupboxLayout.addWidget(widget) - layout.addWidget(groupbox, *stage_info["index"]) - self.widgets[stage_name] = widget - - -class StageControllerApp(qiwis.BaseApp): - """App for monitoring and controlling motorized stages.""" - - def __init__( - self, - name: str, - stages: Dict[str, Dict[str, Any]], - parent: Optional[QObject] = None, - ): - """Extended. - - Args: - stages: Dictionary of stage information. Each key is the name of the - stage and the value is agian a dictionary, whose structure is: - { - "index": [row, column], - "target": ["ip", port, "target_name"] - } - """ - super().__init__(name, parent=parent) - # setup threaded manager - self.thread = QThread() - self.manager = StageManager() - self.proxies = {key: StageProxy(self.manager, key) for key in stages} - self.manager.moveToThread(self.thread) - self.thread.finished.connect(self.manager.deleteLater) - self.thread.finished.connect(self.thread.deleteLater) - self.thread.start() - # timer for periodic position read - self.timer = QTimer(self) - self.timer.start(500) - # setup controller frame - self.frame = StageControllerFrame(stages) - for key, info in stages.items(): - proxy = self.proxies[key] - widget = self.frame.widgets[key] - widget.tryConnect.connect(functools.partial(proxy.connectTarget, tuple(info["target"]))) - widget.moveBy.connect(proxy.moveBy) - widget.moveTo.connect(proxy.moveTo) - # signal connection - self.timer.timeout.connect(self.readAllPositions, type=Qt.QueuedConnection) - self.manager.connectionChanged.connect( - self.handleConnectionChanged, type=Qt.QueuedConnection - ) - self.manager.clientError.connect( - self.handleClientError, type=Qt.QueuedConnection - ) - self.manager.positionReported.connect( - self.handlePositionReported, type=Qt.QueuedConnection - ) - - @pyqtSlot() - def readAllPositions(self): - """Requests positions of all connected stages.""" - for key, widget in self.frame.widgets.items(): - if widget.isConnected(): - self.proxies[key].getPosition() - - @pyqtSlot(str, bool) - def handleConnectionChanged(self, key: str, connected: bool): - """Handles connectionChanged signal. - - Args: - See StageManager.connectionChanged signal. - """ - try: - widget = self.frame.widgets[key] - except KeyError: - logger.exception("Connection changed key does not exist.") - else: - widget.setConnected(connected) - - @pyqtSlot(str, Exception) - def handleClientError(self, key: str, _error: Exception): - """Handles clientError signal. - - Args: - See StageManager.clientError signal. - """ - self.handleConnectionChanged(key, False) - - @pyqtSlot(str, float) - def handlePositionReported(self, key: str, position_m: float): - """Handles positionReported signal. - - Args: - See StageManager.positionReported signal. - """ - try: - widget = self.frame.widgets[key] - except KeyError: - logger.exception("Position reported key does not exist.") - else: - widget.setPosition(position_m) - - def __del__(self): - """Quits the thread before destructing.""" - self.thread.quit() - - def frames(self) -> Tuple[Tuple[str, StageControllerFrame]]: - """Overridden.""" - return (("", self.frame),) From 639f3c83415907a77eb0b1b3e33fcedcba30f4da Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 20:59:15 +0900 Subject: [PATCH 29/38] Rename `connectTarget` -> `openTarget` --- iquip/apps/stage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 76349b3a..b6bbaf5a 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -78,7 +78,7 @@ class StageManager(QObject): clear = pyqtSignal() closeTarget = pyqtSignal(str) - connectTarget = pyqtSignal(str, tuple) + openTarget = pyqtSignal(str, tuple) getPosition = pyqtSignal(str) moveBy = pyqtSignal(str, float) moveTo = pyqtSignal(str, float) @@ -90,7 +90,7 @@ def __init__(self, parent: Optional[QObject] = None): api = ( "clear", "closeTarget", - "connectTarget", + "openTarget", "getPosition", "moveBy", "moveTo", @@ -122,7 +122,7 @@ def _closeTarget(self, key: str): self.connectionChanged.emit(key, False) @pyqtSlot(str, tuple) - def _connectTarget(self, key: str, info: RPCTargetInfo): + def _openTarget(self, key: str, info: RPCTargetInfo): """Creates an RPC client and connects it to the server. Args: From 0c9459c5e3b30beee0532c84391685ddb120788d Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 21:01:24 +0900 Subject: [PATCH 30/38] Rename signals `tryConnect` -> `openTarget` --- iquip/apps/stage.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index b6bbaf5a..e8528bb9 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -207,7 +207,8 @@ class StageWidget(QWidget): # pylint: disable=too-many-instance-attributes position in meters. moveBy(displacement_m): Relative move button is clicked, with the desired displacement in meters. - tryConnect(): Connect button is clicked. + openTarget(): Open button is clicked. + closeTarget(): Close button is clicked. All the displayed values are in mm unit. However, the values for interface (methods and signals) are in m. @@ -215,7 +216,8 @@ class StageWidget(QWidget): # pylint: disable=too-many-instance-attributes moveTo = pyqtSignal(float) moveBy = pyqtSignal(float) - tryConnect = pyqtSignal() + openTarget = pyqtSignal() + closeTarget = pyqtSignal() def __init__(self, parent: Optional[QWidget] = None): """Extended.""" @@ -260,7 +262,7 @@ def __init__(self, parent: Optional[QWidget] = None): layout.addWidget(self.connectButton) layout.addWidget(self._inner) # signal connection - self.connectButton.clicked.connect(self.tryConnect) + self.connectButton.clicked.connect(self.openTarget) self.absoluteButton.clicked.connect(self._absoluteMove) self.relativePositiveButton.clicked.connect(self._relativePositiveMove) self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) From b09d0266a13807f6b494797f14c33898f37013e3 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 21:02:14 +0900 Subject: [PATCH 31/38] Rename `connectButton` -> `connectionButton` --- iquip/apps/stage.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index e8528bb9..47e34bca 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -223,7 +223,7 @@ def __init__(self, parent: Optional[QWidget] = None): """Extended.""" super().__init__(parent=parent) # widgets - self.connectButton = QPushButton("Connect", self) + self.connectionButton = QPushButton("Open", self) self.positionBox = QDoubleSpinBox(self) self.positionBox.setButtonSymbols(QAbstractSpinBox.NoButtons) self.positionBox.setReadOnly(True) @@ -243,7 +243,7 @@ def __init__(self, parent: Optional[QWidget] = None): self.relativeBox.setAlignment(Qt.AlignRight) self.relativePositiveButton = QPushButton("Move +", self) self.relativeNegativeButton = QPushButton("Move -", self) - self._inner = QWidget(self) # except connectButton + self._inner = QWidget(self) # except connectionButton # layout abosluteLayout = QVBoxLayout() abosluteLayout.addWidget(self.absoluteBox) @@ -259,10 +259,10 @@ def __init__(self, parent: Optional[QWidget] = None): innerLayout.addWidget(self.positionBox) innerLayout.addLayout(moveLayout) layout = QVBoxLayout(self) - layout.addWidget(self.connectButton) + layout.addWidget(self.connectionButton) layout.addWidget(self._inner) # signal connection - self.connectButton.clicked.connect(self.openTarget) + self.connectionButton.clicked.connect(self.openTarget) self.absoluteButton.clicked.connect(self._absoluteMove) self.relativePositiveButton.clicked.connect(self._relativePositiveMove) self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) @@ -279,8 +279,8 @@ def setConnected(self, connected: bool): connected: True for connected, False for disconnected. """ self._inner.setEnabled(connected) - self.connectButton.setEnabled(not connected) - self.connectButton.setText("Connected" if connected else "Connect") + self.connectionButton.setEnabled(not connected) + self.connectionButton.setText("Connected" if connected else "Connect") def isConnected(self) -> bool: """Returns whether the client is currently connected.""" From 5a9da4d056b3de5cbec44f32375d1a2f0587d7c4 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Tue, 2 Apr 2024 21:10:57 +0900 Subject: [PATCH 32/38] Set connection button checkable --- iquip/apps/stage.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 47e34bca..5f4b3788 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -224,6 +224,7 @@ def __init__(self, parent: Optional[QWidget] = None): super().__init__(parent=parent) # widgets self.connectionButton = QPushButton("Open", self) + self.connectionButton.setCheckable(True) self.positionBox = QDoubleSpinBox(self) self.positionBox.setButtonSymbols(QAbstractSpinBox.NoButtons) self.positionBox.setReadOnly(True) @@ -262,7 +263,9 @@ def __init__(self, parent: Optional[QWidget] = None): layout.addWidget(self.connectionButton) layout.addWidget(self._inner) # signal connection - self.connectionButton.clicked.connect(self.openTarget) + self.connectionButton.clicked.connect( + functools.partial(self.connectionButton.setEnabled, False)) + self.connectionButton.clicked.connect(self._connectionButtonClicked) self.absoluteButton.clicked.connect(self._absoluteMove) self.relativePositiveButton.clicked.connect(self._relativePositiveMove) self.relativeNegativeButton.clicked.connect(self._relativeNegativeMove) @@ -270,21 +273,33 @@ def __init__(self, parent: Optional[QWidget] = None): self.setConnected(False) @pyqtSlot(bool) - def setConnected(self, connected: bool): + def setConnected(self, open: bool): """Sets the current connection status. - This also changes the enabled status and the connect button text. + This also changes the enabled status and the connection button text. Args: - connected: True for connected, False for disconnected. + open: True for open, False for closed. """ - self._inner.setEnabled(connected) - self.connectionButton.setEnabled(not connected) - self.connectionButton.setText("Connected" if connected else "Connect") + self._inner.setEnabled(open) + self.connectionButton.setEnabled(True) + self.connectionButton.setText("Close" if open else "Open") def isConnected(self) -> bool: """Returns whether the client is currently connected.""" - return self._inner.isEnabled() + return self.connectionButton.isChecked() + + @pyqtSlot(bool) + def _connectionButtonClicked(self, checked: bool): + """Connection button is clicked. + + Args: + checked: True for opening the target, False for closing. + """ + if checked: + self.openTarget.emit() + else: + self.closeTarget.emit() @pyqtSlot(float) def setPosition(self, position_m: float): From 250a5b02b2ecd326b809aa759622039d05c8aa4c Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Wed, 3 Apr 2024 15:56:33 +0900 Subject: [PATCH 33/38] Avoid redefining `open` Resolved the pylint warning. --- iquip/apps/stage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 5f4b3788..94ae4789 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -273,17 +273,17 @@ def __init__(self, parent: Optional[QWidget] = None): self.setConnected(False) @pyqtSlot(bool) - def setConnected(self, open: bool): + def setConnected(self, open_: bool): """Sets the current connection status. This also changes the enabled status and the connection button text. Args: - open: True for open, False for closed. + open_: True for open, False for closed. """ - self._inner.setEnabled(open) + self._inner.setEnabled(open_) self.connectionButton.setEnabled(True) - self.connectionButton.setText("Close" if open else "Open") + self.connectionButton.setText("Close" if open_ else "Open") def isConnected(self) -> bool: """Returns whether the client is currently connected.""" From 248d00adb8e8288df103b4fbfc8f5bcde588c9a4 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Thu, 4 Apr 2024 12:18:54 +0900 Subject: [PATCH 34/38] Re-order imported classes in alphabetical order Applied the review. --- iquip/apps/stage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 94ae4789..892f6031 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -9,8 +9,7 @@ from sipyco.pc_rpc import Client from PyQt5.QtCore import QObject, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QAbstractSpinBox, QDoubleSpinBox, QPushButton, QWidget, - QVBoxLayout, QHBoxLayout, + QAbstractSpinBox, QDoubleSpinBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget, ) logger = logging.getLogger(__name__) From e4b8ce196adcff480b8947f1d9e6471ceabb6138 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Thu, 4 Apr 2024 12:20:04 +0900 Subject: [PATCH 35/38] Re-order imported classes --- iquip/apps/stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 892f6031..4a150289 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -7,7 +7,7 @@ from typing import Callable, Dict, Optional, Tuple from sipyco.pc_rpc import Client -from PyQt5.QtCore import QObject, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt from PyQt5.QtWidgets import ( QAbstractSpinBox, QDoubleSpinBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget, ) From 8bd214a7ddd7d81cede6f5da1d8dde58d56d5e9d Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Thu, 4 Apr 2024 12:20:37 +0900 Subject: [PATCH 36/38] Re-order import --- iquip/apps/stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 4a150289..985982ad 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -6,11 +6,11 @@ import logging from typing import Callable, Dict, Optional, Tuple -from sipyco.pc_rpc import Client from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt from PyQt5.QtWidgets import ( QAbstractSpinBox, QDoubleSpinBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget, ) +from sipyco.pc_rpc import Client logger = logging.getLogger(__name__) From 7979bdaf2286293783ecccf0da2e34f0f16efc79 Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Thu, 4 Apr 2024 12:32:41 +0900 Subject: [PATCH 37/38] Add attribute docstring of `StageWidget` --- iquip/apps/stage.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 985982ad..5ed219bd 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -201,6 +201,15 @@ def __getattr__(self, name: str) -> Callable: class StageWidget(QWidget): # pylint: disable=too-many-instance-attributes """UI for stage control. + Attributes: + connectionButton: Button for toggling rpc connection. + positionBox: Spinbox displaying the current position (read-only). + absoluteBox: Spinbox for absolute move destination. + absoluteButton: Button for absolute move. + relativeBox: Spinbox for relative move step size. + relativePositiveButton: Button for relative move in positive direction. + relativeNegativeButton: Button for relative move in negative direction. + Signals: moveTo(position_m): Absolute move button is clicked, with the destination position in meters. From 307e870c42ab7f9a8ce979451699db7918f485db Mon Sep 17 00:00:00 2001 From: Jiyong Kang Date: Thu, 4 Apr 2024 12:33:25 +0900 Subject: [PATCH 38/38] Fix typo Applied the reivew. --- iquip/apps/stage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iquip/apps/stage.py b/iquip/apps/stage.py index 5ed219bd..c4cac45b 100644 --- a/iquip/apps/stage.py +++ b/iquip/apps/stage.py @@ -254,15 +254,15 @@ def __init__(self, parent: Optional[QWidget] = None): self.relativeNegativeButton = QPushButton("Move -", self) self._inner = QWidget(self) # except connectionButton # layout - abosluteLayout = QVBoxLayout() - abosluteLayout.addWidget(self.absoluteBox) - abosluteLayout.addWidget(self.absoluteButton) + absoluteLayout = QVBoxLayout() + absoluteLayout.addWidget(self.absoluteBox) + absoluteLayout.addWidget(self.absoluteButton) relativeLayout = QVBoxLayout() relativeLayout.addWidget(self.relativePositiveButton) relativeLayout.addWidget(self.relativeBox) relativeLayout.addWidget(self.relativeNegativeButton) moveLayout = QHBoxLayout() - moveLayout.addLayout(abosluteLayout) + moveLayout.addLayout(absoluteLayout) moveLayout.addLayout(relativeLayout) innerLayout = QVBoxLayout(self._inner) innerLayout.addWidget(self.positionBox)