diff --git a/controller/controller_tool/tool/src/py/Controller.py b/controller/controller_tool/tool/src/py/Controller.py index 57f0d78b5..4806a2101 100644 --- a/controller/controller_tool/tool/src/py/Controller.py +++ b/controller/controller_tool/tool/src/py/Controller.py @@ -127,7 +127,7 @@ def is_valid_dds_domain(self, dds_domain): """Check if DDS Domain is valid.""" return ((dds_domain >= 0) and (dds_domain <= MAX_DDS_DOMAIN_ID)) - def init_dds(self, dds_domain): + def init_dds(self, dds_domain, command_topic, status_topic): # Check DDS Domain if (not self.is_valid_dds_domain(dds_domain)): raise ValueError( @@ -151,7 +151,7 @@ def init_dds(self, dds_domain): command_topic_qos = fastdds.TopicQos() self.participant.get_default_topic_qos(command_topic_qos) self.command_topic = self.participant.create_topic( - '/ddsrecorder/command', + command_topic, self.command_topic_data_type.getName(), command_topic_qos) @@ -164,7 +164,7 @@ def init_dds(self, dds_domain): status_topic_qos = fastdds.TopicQos() self.participant.get_default_topic_qos(status_topic_qos) self.status_topic = self.participant.create_topic( - '/ddsrecorder/status', + status_topic, self.status_topic_data_type.getName(), status_topic_qos) diff --git a/controller/controller_tool/tool/src/py/ControllerGUI.py b/controller/controller_tool/tool/src/py/ControllerGUI.py index 174acbd24..e1668cb1c 100644 --- a/controller/controller_tool/tool/src/py/ControllerGUI.py +++ b/controller/controller_tool/tool/src/py/ControllerGUI.py @@ -23,7 +23,7 @@ from PyQt6.QtGui import QAction, QDesktopServices from PyQt6.QtWidgets import ( QDialog, QDialogButtonBox, QHBoxLayout, QHeaderView, - QLabel, QMainWindow, QMenuBar, QPushButton, QSpinBox, QTableWidget, + QLabel, QLineEdit, QMainWindow, QMenuBar, QPushButton, QSpinBox, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget) DDS_RECORDER = 'DDS Recorder' @@ -83,6 +83,56 @@ def get_dds_domain(self): return int(self.spin_box.value()) +class DdsTopicNameDialog(QDialog): + """Class that implements the a dialog to set the DDS command and status topic names.""" + + def __init__(self, current_command_topic, current_status_topic): + """Construct the dialog to set the DDS command and status topic names.""" + super().__init__() + + self.command_topic_label = QLabel('Command topic:') + self.command_topic_label.setFixedWidth(120) + self.command_text_box = QLineEdit(self) + self.command_text_box.setText(current_command_topic) + self.command_text_box.setMinimumWidth(250) + + self.status_topic_label = QLabel('Status topic:') + self.status_topic_label.setFixedWidth(120) + self.status_text_box = QLineEdit(self) + self.status_text_box.setText(current_status_topic) + self.status_text_box.setMinimumWidth(250) + + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Save + | QDialogButtonBox.StandardButton.Cancel) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.command_layout = QHBoxLayout() + self.command_layout.addWidget(self.command_topic_label) + self.command_layout.addWidget(self.command_text_box) + + self.status_layout = QHBoxLayout() + self.status_layout.addWidget(self.status_topic_label) + self.status_layout.addWidget(self.status_text_box) + + layout = QVBoxLayout() + layout.addLayout(self.command_layout) + layout.addLayout(self.status_layout) + layout.addWidget(self.buttonBox) + layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetFixedSize) + + self.setLayout(layout) + + def get_command_topic(self): + """Return DDS command topic from the text box.""" + return self.command_text_box.text() + + def get_status_topic(self): + """Return DDS status topic from the text box.""" + return self.status_text_box.text() + + class MenuWidget(QMenuBar): """Class that implements the menu of DDS Recorder controller GUI.""" @@ -99,6 +149,10 @@ def __init__(self, main_window): dds_domain_action.triggered.connect(self.main_window.dds_domain_dialog) file_menu.addAction(dds_domain_action) + topic_name_action = QAction('DDS Topics', self) + topic_name_action.triggered.connect(self.main_window.topic_name_dialog) + file_menu.addAction(topic_name_action) + docs_action = QAction('Documentation', self) docs_action.triggered.connect( lambda: QDesktopServices.openUrl( @@ -145,6 +199,8 @@ def __init__(self): self.dds_controller = Controller() self.dds_domain = 0 + self.command_topic = '/ddsrecorder/command' + self.status_topic = '/ddsrecorder/status' self.init_gui() @@ -156,7 +212,7 @@ def __init__(self): # Create DDS entities at the end to avoid race condition (receive # status before connecting with signal slots) - self.dds_controller.init_dds(self.dds_domain) + self.dds_controller.init_dds(self.dds_domain, self.command_topic, self.status_topic) def on_ddsrecorder_discovered(self, discovered, message): """Inform that a new DDS Recorder has been discovered.""" @@ -178,17 +234,25 @@ def on_ddsrecorder_status(self, previous_status, current_status, info): # Change status bar self.update_status(RecorderStatus[current_status.upper()]) - def restart_controller(self, dds_domain=0): - """Restart the DDS Controller if the DDS Domain changes.""" - if dds_domain != self.dds_domain: + def restart_controller( + self, + dds_domain=0, + command_topic='/ddsrecorder/command', + status_topic='/ddsrecorder/status'): + """Restart the DDS Controller if the DDS Domain or topic name changes.""" + if (dds_domain != self.dds_domain + or command_topic != self.command_topic + or status_topic != self.status_topic): if self.dds_controller.is_valid_dds_domain(dds_domain): # Delete DDS entities in previous domain self.dds_controller.delete_dds() # Reset status self.update_status(RecorderStatus.CLOSED) # Create DDS entities in new domain - self.dds_controller.init_dds(dds_domain) + self.dds_controller.init_dds(dds_domain, command_topic, status_topic) self.dds_domain = dds_domain + self.command_topic = command_topic + self.status_topic = status_topic def init_gui(self): """Initialize the graphical interface and its widgets.""" @@ -317,7 +381,17 @@ def dds_domain_dialog(self): # Restart only if it is a different Domain if domain != self.dds_domain: if (self.dds_controller.is_valid_dds_domain(domain)): - self.restart_controller(dds_domain=domain) + self.restart_controller(domain, self.command_topic, self.status_topic) + + def topic_name_dialog(self): + """Create a dialog to update the recorder topic names.""" + dialog = DdsTopicNameDialog(self.command_topic, self.status_topic) + if dialog.exec(): + command_topic = dialog.get_command_topic() + status_topic = dialog.get_status_topic() + # Restart controller if a topic name has changed + if command_topic != self.command_topic or status_topic != self.status_topic: + self.restart_controller(self.dds_domain, command_topic, status_topic) def event_start_button_clicked(self): """Publish command.""" diff --git a/docs/rst/recording/remote_control/remote_control.rst b/docs/rst/recording/remote_control/remote_control.rst index e63438bd6..3f9bd04cd 100644 --- a/docs/rst/recording/remote_control/remote_control.rst +++ b/docs/rst/recording/remote_control/remote_control.rst @@ -182,6 +182,8 @@ If the controller should function in a domain different than the default one (`` .. figure:: /rst/figures/controller_domain.png :align: center +It is also possible to use non-default status and command topic names through the ``File->DDS Topics`` dialog. + When a |ddsrecorder| instance is found in the domain, a message is displayed in the logging panel: .. figure:: /rst/figures/controller_found.png