controller_manager.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # Copyright (C) 2013, Georgia Tech
3 # Copyright (C) 2015, PAL Robotics S.L.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 # * Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 # * Redistributions in binary form must reproduce the above copyright
10 # notice, this list of conditions and the following disclaimer in the
11 # documentation and/or other materials provided with the distribution.
12 # * Neither the name of PAL Robotics S.L. nor the names of its
13 # contributors may be used to endorse or promote products derived from
14 # this software without specific prior written permission.
15 #
16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 # POSSIBILITY OF SUCH DAMAGE.
27 
28 import os
29 import rospkg
30 import rospy
31 
32 from python_qt_binding import loadUi
33 from python_qt_binding.QtCore import QAbstractTableModel, QModelIndex, Qt,\
34  QTimer, QVariant, Signal
35 from python_qt_binding.QtWidgets import QWidget, QFormLayout, QHeaderView,\
36  QMenu, QStyledItemDelegate
37 from python_qt_binding.QtGui import QCursor, QFont, QIcon, QStandardItem,\
38  QStandardItemModel
39 from qt_gui.plugin import Plugin
40 
41 from controller_manager_msgs.msg import ControllerState
42 from controller_manager_msgs.srv import *
43 from controller_manager_msgs.utils\
44  import ControllerLister, ControllerManagerLister,\
45  get_rosparam_controller_names
46 
47 from .update_combo import update_combo
48 
49 
51  """
52  Graphical frontend for managing ros_control controllers.
53  """
54  _cm_update_freq = 1 # Hz
55 
56  def __init__(self, context):
57  super(ControllerManager, self).__init__(context)
58  self.setObjectName('ControllerManager')
59 
60  # Create QWidget and extend it with all the attributes and children
61  # from the UI file
62  self._widget = QWidget()
63  rp = rospkg.RosPack()
64  ui_file = os.path.join(rp.get_path('rqt_controller_manager'),
65  'resource',
66  'controller_manager.ui')
67  loadUi(ui_file, self._widget)
68  self._widget.setObjectName('ControllerManagerUi')
69 
70  # Pop-up that displays controller information
71  self._popup_widget = QWidget()
72  ui_file = os.path.join(rp.get_path('rqt_controller_manager'),
73  'resource',
74  'controller_info.ui')
75  loadUi(ui_file, self._popup_widget)
76  self._popup_widget.setObjectName('ControllerInfoUi')
77 
78  # Show _widget.windowTitle on left-top of each plugin (when
79  # it's set in _widget). This is useful when you open multiple
80  # plugins at once. Also if you open multiple instances of your
81  # plugin at once, these lines add number to make it easy to
82  # tell from pane to pane.
83  if context.serial_number() > 1:
84  self._widget.setWindowTitle(self._widget.windowTitle() +
85  (' (%d)' % context.serial_number()))
86  # Add widget to the user interface
87  context.add_widget(self._widget)
88 
89  # Initialize members
90  self._cm_ns = [] # Namespace of the selected controller manager
91  self._controllers = [] # State of each controller
92  self._table_model = None
93  self._controller_lister = None
94 
95  # Controller manager service proxies
96  self._load_srv = None
97  self._unload_srv = None
98  self._switch_srv = None
99 
100  # Controller state icons
101  rospack = rospkg.RosPack()
102  path = rospack.get_path('rqt_controller_manager')
103  self._icons = {'running': QIcon(path + '/resource/led_green.png'),
104  'stopped': QIcon(path + '/resource/led_red.png'),
105  'uninitialized': QIcon(path + '/resource/led_off.png'),
106  'initialized': QIcon(path + '/resource/led_red.png')}
107 
108  # Controllers display
109  table_view = self._widget.table_view
110  table_view.setContextMenuPolicy(Qt.CustomContextMenu)
111  table_view.customContextMenuRequested.connect(self._on_ctrl_menu)
112 
113  table_view.doubleClicked.connect(self._on_ctrl_info)
114 
115  header = table_view.horizontalHeader()
116  header.setSectionResizeMode(QHeaderView.ResizeToContents)
117  header.setContextMenuPolicy(Qt.CustomContextMenu)
118  header.customContextMenuRequested.connect(self._on_header_menu)
119 
120  # Timer for controller manager updates
121  self._list_cm = ControllerManagerLister()
122  self._update_cm_list_timer = QTimer(self)
123  self._update_cm_list_timer.setInterval(1000.0 /
124  self._cm_update_freq)
125  self._update_cm_list_timer.timeout.connect(self._update_cm_list)
126  self._update_cm_list_timer.start()
127 
128  # Timer for running controller updates
129  self._update_ctrl_list_timer = QTimer(self)
130  self._update_ctrl_list_timer.setInterval(1000.0 /
131  self._cm_update_freq)
132  self._update_ctrl_list_timer.timeout.connect(self._update_controllers)
133  self._update_ctrl_list_timer.start()
134 
135  # Signal connections
136  w = self._widget
137  w.cm_combo.currentIndexChanged[str].connect(self._on_cm_change)
138 
139  def shutdown_plugin(self):
140  self._update_cm_list_timer.stop()
141  self._update_ctrl_list_timer.stop()
142  self._popup_widget.hide()
143 
144  def save_settings(self, plugin_settings, instance_settings):
145  instance_settings.set_value('cm_ns', self._cm_ns)
146 
147  def restore_settings(self, plugin_settings, instance_settings):
148  # Restore last session's controller_manager, if present
149  self._update_cm_list()
150  cm_ns = instance_settings.value('cm_ns')
151  cm_combo = self._widget.cm_combo
152  cm_list = [cm_combo.itemText(i) for i in range(cm_combo.count())]
153  try:
154  idx = cm_list.index(cm_ns)
155  cm_combo.setCurrentIndex(idx)
156  except (ValueError):
157  pass
158 
159  # def trigger_configuration(self):
160  # Comment in to signal that the plugin has a way to configure
161  # This will enable a setting button (gear icon) in each dock widget
162  # title bar
163  # Usually used to open a modal configuration dialog
164 
165  def _update_cm_list(self):
166  update_combo(self._widget.cm_combo, self._list_cm())
167 
168  def _on_cm_change(self, cm_ns):
169  self._cm_ns = cm_ns
170 
171  # Setup services for communicating with the selected controller manager
172  self._set_cm_services(cm_ns)
173 
174  # Controller lister for the selected controller manager
175  if cm_ns:
176  self._controller_lister = ControllerLister(cm_ns)
177  self._update_controllers()
178  else:
179  self._controller_lister = None
180 
181  def _set_cm_services(self, cm_ns):
182  if cm_ns:
183  # NOTE: Persistent services are used for performance reasons.
184  # If the controller manager dies, we detect it and disconnect from
185  # it anyway
186  load_srv_name = _append_ns(cm_ns, 'load_controller')
187  self._load_srv = rospy.ServiceProxy(load_srv_name,
188  LoadController,
189  persistent=True)
190  unload_srv_name = _append_ns(cm_ns, 'unload_controller')
191  self._unload_srv = rospy.ServiceProxy(unload_srv_name,
192  UnloadController,
193  persistent=True)
194  switch_srv_name = _append_ns(cm_ns, 'switch_controller')
195  self._switch_srv = rospy.ServiceProxy(switch_srv_name,
196  SwitchController,
197  persistent=True)
198  else:
199  self._load_srv = None
200  self._unload_srv = None
201  self._switch_srv = None
202 
204  # Find controllers associated to the selected controller manager
205  controllers = self._list_controllers()
206 
207  # Update controller display, if necessary
208  if self._controllers != controllers:
209  self._controllers = controllers
210  self._show_controllers() # NOTE: Model is recomputed from scratch
211 
212  def _list_controllers(self):
213  """
214  @return List of controllers associated to a controller manager
215  namespace. Contains both stopped/running controllers, as returned by
216  the C{list_controllers} service, plus uninitialized controllers with
217  configurations loaded in the parameter server.
218  @rtype [str]
219  """
220  if not self._cm_ns:
221  return []
222 
223  # Add loaded controllers first
224  controllers = self._controller_lister()
225 
226  # Append potential controller configs found in the parameter server
227  all_ctrls_ns = _resolve_controllers_ns(self._cm_ns)
228  for name in get_rosparam_controller_names(all_ctrls_ns):
229  add_ctrl = not any(name == ctrl.name for ctrl in controllers)
230  if add_ctrl:
231  type_str = _rosparam_controller_type(all_ctrls_ns, name)
232  uninit_ctrl = ControllerState(name=name,
233  type=type_str,
234  state='uninitialized')
235  controllers.append(uninit_ctrl)
236  return controllers
237 
238  def _show_controllers(self):
239  table_view = self._widget.table_view
241  table_view.setModel(self._table_model)
242 
243  def _on_ctrl_menu(self, pos):
244  # Get data of selected controller
245  row = self._widget.table_view.rowAt(pos.y())
246  if row < 0:
247  return # Cursor is not under a valid item
248 
249  ctrl = self._controllers[row]
250 
251  # Show context menu
252  menu = QMenu(self._widget.table_view)
253  if ctrl.state == 'running':
254  action_stop = menu.addAction(self._icons['stopped'], 'Stop')
255  action_kill = menu.addAction(self._icons['uninitialized'],
256  'Stop and Unload')
257  elif ctrl.state == 'stopped':
258  action_start = menu.addAction(self._icons['running'],
259  'Start again')
260  action_unload = menu.addAction(self._icons['uninitialized'],
261  'Unload')
262  elif ctrl.state == 'initialized':
263  action_start = menu.addAction(self._icons['running'], 'Start')
264  action_unload = menu.addAction(self._icons['uninitialized'],
265  'Unload')
266  elif ctrl.state == 'uninitialized':
267  action_load = menu.addAction(self._icons['stopped'], 'Load')
268  action_spawn = menu.addAction(self._icons['running'],
269  'Load and Start')
270 
271  action = menu.exec_(self._widget.table_view.mapToGlobal(pos))
272 
273  # Evaluate user action
274  if ctrl.state == 'running':
275  if action is action_stop:
276  self._stop_controller(ctrl.name)
277  elif action is action_kill:
278  self._stop_controller(ctrl.name)
279  self._unload_controller(ctrl.name)
280  elif ctrl.state == 'stopped' or ctrl.state == 'initialized':
281  if action is action_start:
282  self._start_controller(ctrl.name)
283  elif action is action_unload:
284  self._unload_controller(ctrl.name)
285  elif ctrl.state == 'uninitialized':
286  if action is action_load:
287  self._load_controller(ctrl.name)
288  if action is action_spawn:
289  self._load_controller(ctrl.name)
290  self._start_controller(ctrl.name)
291 
292  def _on_ctrl_info(self, index):
293  popup = self._popup_widget
294 
295  ctrl = self._controllers[index.row()]
296  popup.ctrl_name.setText(ctrl.name)
297  popup.ctrl_type.setText(ctrl.type)
298 
299  res_model = QStandardItemModel()
300  model_root = QStandardItem('Claimed Resources')
301  res_model.appendRow(model_root)
302  for hw_res in ctrl.claimed_resources:
303  hw_iface_item = QStandardItem(hw_res.hardware_interface)
304  model_root.appendRow(hw_iface_item)
305  for res in hw_res.resources:
306  res_item = QStandardItem(res)
307  hw_iface_item.appendRow(res_item)
308 
309  popup.resource_tree.setModel(res_model)
310  popup.resource_tree.setItemDelegate(FontDelegate(popup.resource_tree))
311  popup.resource_tree.expandAll()
312  popup.move(QCursor.pos())
313  popup.show()
314 
315  def _on_header_menu(self, pos):
316  header = self._widget.table_view.horizontalHeader()
317 
318  # Show context menu
319  menu = QMenu(self._widget.table_view)
320  action_toggle_auto_resize = menu.addAction('Toggle Auto-Resize')
321  action = menu.exec_(header.mapToGlobal(pos))
322 
323  # Evaluate user action
324  if action is action_toggle_auto_resize:
325  if header.resizeMode(0) == QHeaderView.ResizeToContents:
326  header.setSectionResizeMode(QHeaderView.Interactive)
327  else:
328  header.setSectionResizeMode(QHeaderView.ResizeToContents)
329 
330  def _load_controller(self, name):
331  self._load_srv.call(LoadControllerRequest(name=name))
332 
333  def _unload_controller(self, name):
334  self._unload_srv.call(UnloadControllerRequest(name=name))
335 
336  def _start_controller(self, name):
337  strict = SwitchControllerRequest.STRICT
338  req = SwitchControllerRequest(start_controllers=[name],
339  stop_controllers=[],
340  strictness=strict)
341  self._switch_srv.call(req)
342 
343  def _stop_controller(self, name):
344  strict = SwitchControllerRequest.STRICT
345  req = SwitchControllerRequest(start_controllers=[],
346  stop_controllers=[name],
347  strictness=strict)
348  self._switch_srv.call(req)
349 
350 
351 class ControllerTable(QAbstractTableModel):
352  """
353  Model containing controller information for tabular display.
354 
355  The model allows display of basic read-only information like controller
356  name and state.
357  """
358  def __init__(self, controller_info, icons, parent=None):
359  QAbstractTableModel.__init__(self, parent)
360  self._data = controller_info
361  self._icons = icons
362 
363  def rowCount(self, parent):
364  return len(self._data)
365 
366  def columnCount(self, parent):
367  return 2
368 
369  def headerData(self, col, orientation, role):
370  if orientation == Qt.Horizontal and role == Qt.DisplayRole:
371  if col == 0:
372  return 'controller'
373  elif col == 1:
374  return 'state'
375  else:
376  return None
377 
378  def data(self, index, role):
379  if not index.isValid():
380  return None
381 
382  ctrl = self._data[index.row()]
383 
384  if role == Qt.DisplayRole:
385  if index.column() == 0:
386  return ctrl.name
387  elif index.column() == 1:
388  return ctrl.state
389 
390  if role == Qt.DecorationRole:
391  if index.column() == 0:
392  return self._icons[ctrl.state]
393 
394  if role == Qt.FontRole:
395  if index.column() == 0:
396  bf = QFont()
397  bf.setBold(True)
398  return bf
399 
400  if role == Qt.TextAlignmentRole:
401  if index.column() == 1:
402  return Qt.AlignCenter
403 
404 
405 class FontDelegate(QStyledItemDelegate):
406  """
407  Simple delegate for customizing font weight and italization when
408  displaying resources claimed by a controller
409  """
410  def paint(self, painter, option, index):
411  if not index.parent().isValid():
412  # Root level
413  option.font.setWeight(QFont.Bold)
414  if index.parent().isValid() and not index.parent().parent().isValid():
415  # Hardware interface level
416  option.font.setItalic(True)
417  option.font.setWeight(QFont.Bold)
418  QStyledItemDelegate.paint(self, painter, option, index)
419 
420 
422  """
423  Resolve the namespace containing controller configurations from that of
424  the controller manager.
425  Controllers are assumed to live one level above the controller
426  manager, e.g.
427 
428  >>> _resolve_controller_ns('/path/to/controller_manager')
429  '/path/to'
430 
431  In the particular case in which the controller manager is not
432  namespaced, the controller is assumed to live in the root namespace
433 
434  >>> _resolve_controller_ns('/')
435  '/'
436  >>> _resolve_controller_ns('')
437  '/'
438  @param cm_ns Controller manager namespace (can be an empty string)
439  @type cm_ns str
440  @return Controllers namespace
441  @rtype str
442  """
443  ns = cm_ns.rsplit('/', 1)[0]
444  if not ns:
445  ns += '/'
446  return ns
447 
448 
449 def _append_ns(in_ns, suffix):
450  """
451  Append a sub-namespace (suffix) to the input namespace
452  @param in_ns Input namespace
453  @type in_ns str
454  @return Suffix namespace
455  @rtype str
456  """
457  ns = in_ns
458  if ns[-1] != '/':
459  ns += '/'
460  ns += suffix
461  return ns
462 
463 
464 def _rosparam_controller_type(ctrls_ns, ctrl_name):
465  """
466  Get a controller's type from its ROS parameter server configuration
467  @param ctrls_ns Namespace where controllers should be located
468  @type ctrls_ns str
469  @param ctrl_name Controller name
470  @type ctrl_name str
471  @return Controller type
472  @rtype str
473  """
474  type_param = _append_ns(ctrls_ns, ctrl_name) + '/type'
475  return rospy.get_param(type_param)
def __init__(self, controller_info, icons, parent=None)
def save_settings(self, plugin_settings, instance_settings)
def update_combo(combo, new_vals)
Definition: update_combo.py:28
def _rosparam_controller_type(ctrls_ns, ctrl_name)
def restore_settings(self, plugin_settings, instance_settings)


rqt_controller_manager
Author(s): Kelsey Hawkins , Adolfo Rodríguez Tsouroukdissian
autogenerated on Mon Feb 28 2022 23:30:24