service_caller_widget.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 # * Neither the name of the TU Darmstadt nor the names of its
17 # contributors may be used to endorse or promote products derived
18 # from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 # POSSIBILITY OF SUCH DAMAGE.
32 
33 from __future__ import division
34 import math
35 import os
36 import random
37 import time
38 
39 from python_qt_binding import loadUi
40 from python_qt_binding.QtCore import Qt, Slot, qWarning
41 from python_qt_binding.QtGui import QIcon
42 from python_qt_binding.QtWidgets import QMenu, QTreeWidgetItem, QWidget
43 
44 import rospkg
45 import rospy
46 import genpy
47 import rosservice
48 
49 from rqt_py_common.extended_combo_box import ExtendedComboBox
50 
51 
52 class ServiceCallerWidget(QWidget):
53  column_names = ['service', 'type', 'expression']
54 
55  def __init__(self):
56  super(ServiceCallerWidget, self).__init__()
57  self.setObjectName('ServiceCallerWidget')
58 
59  # create context for the expression eval statement
60  self._eval_locals = {}
61  for module in (math, random, time):
62  self._eval_locals.update(module.__dict__)
63  self._eval_locals['genpy'] = genpy
64  del self._eval_locals['__name__']
65  del self._eval_locals['__doc__']
66 
67  rp = rospkg.RosPack()
68  ui_file = os.path.join(rp.get_path('rqt_service_caller'), 'resource', 'ServiceCaller.ui')
69  loadUi(ui_file, self, {'ExtendedComboBox': ExtendedComboBox})
70  self.refresh_services_button.setIcon(QIcon.fromTheme('view-refresh'))
71  self.call_service_button.setIcon(QIcon.fromTheme('call-start'))
72 
73  self._column_index = {}
74  for column_name in self.column_names:
75  self._column_index[column_name] = len(self._column_index)
76 
77  self._service_info = None
79 
80  self.request_tree_widget.itemChanged.connect(self.request_tree_widget_itemChanged)
81 
82  def save_settings(self, plugin_settings, instance_settings):
83  instance_settings.set_value('current_service_name', self._service_info['service_name'])
84  instance_settings.set_value('splitter_orientation', self.splitter.orientation())
85 
86  def restore_settings(self, plugin_settings, instance_settings):
87  current_service_name = instance_settings.value('current_service_name', None)
88  if current_service_name:
89  current_service_index = self.service_combo_box.findData(
90  current_service_name, Qt.DisplayRole)
91  if current_service_index != -1:
92  self.service_combo_box.setCurrentIndex(current_service_index)
93 
94  if int(instance_settings.value('splitter_orientation', Qt.Vertical)) == int(Qt.Vertical):
95  self.splitter.setOrientation(Qt.Vertical)
96  else:
97  self.splitter.setOrientation(Qt.Horizontal)
98 
100  new_orientation = \
101  Qt.Vertical if self.splitter.orientation() == Qt.Horizontal else Qt.Horizontal
102  self.splitter.setOrientation(new_orientation)
103 
104  @Slot()
106  service_names = rosservice.get_service_list()
107  self._services = {}
108  for service_name in service_names:
109  try:
110  self._services[service_name] = rosservice.get_service_class_by_name(service_name)
111  # qDebug('ServiceCaller.on_refresh_services_button_clicked(): found
112  # service %s using class %s' % (service_name,
113  # self._services[service_name]))
114  except (rosservice.ROSServiceException, rosservice.ROSServiceIOException) as e:
115  qWarning(
116  'ServiceCaller.on_refresh_services_button_clicked(): could not get class of service %s:\n%s' %
117  (service_name, e))
118  except Exception as e:
119  qWarning(
120  'ServiceCaller.on_refresh_services_button_clicked(): failed to load class of service %s:\n%s' %
121  (service_name, e))
122 
123  self.service_combo_box.clear()
124  self.service_combo_box.addItems(sorted(self._services.keys()))
125 
126  @Slot(str)
128  self.request_tree_widget.clear()
129  self.response_tree_widget.clear()
130  service_name = str(service_name)
131  if not service_name:
132  return
133 
134  self._service_info = {}
135  self._service_info['service_name'] = service_name
136  self._service_info['service_class'] = self._services[service_name]
137  self._service_info['service_proxy'] = rospy.ServiceProxy(
138  service_name, self._service_info['service_class'])
139  self._service_info['expressions'] = {}
140  self._service_info['counter'] = 0
141 
142  # recursively create widget items for the service request's slots
143  request_class = self._service_info['service_class']._request_class
144  top_level_item = self._recursive_create_widget_items(
145  None, service_name, request_class._type, request_class())
146 
147  # add top level item to tree widget
148  self.request_tree_widget.addTopLevelItem(top_level_item)
149 
150  # resize columns
151  self.request_tree_widget.expandAll()
152  for i in range(self.request_tree_widget.columnCount()):
153  self.request_tree_widget.resizeColumnToContents(i)
154 
155  def _recursive_create_widget_items(self, parent, topic_name, type_name, message, is_editable=True):
156  item = QTreeWidgetItem(parent)
157  if is_editable:
158  item.setFlags(item.flags() | Qt.ItemIsEditable)
159  else:
160  item.setFlags(item.flags() & (~Qt.ItemIsEditable))
161 
162  if parent is None:
163  # show full topic name with preceding namespace on toplevel item
164  topic_text = topic_name
165  else:
166  topic_text = topic_name.split('/')[-1]
167 
168  item.setText(self._column_index['service'], topic_text)
169  item.setText(self._column_index['type'], type_name)
170 
171  item.setData(0, Qt.UserRole, topic_name)
172 
173  if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
174  for slot_name, type_name in zip(message.__slots__, message._slot_types):
175  self._recursive_create_widget_items(item, topic_name + '/' + slot_name,
176  type_name, getattr(message, slot_name), is_editable)
177 
178  elif type(message) in (list, tuple) and (len(message) > 0) and hasattr(message[0], '__slots__'):
179  type_name = type_name.split('[', 1)[0]
180  for index, slot in enumerate(message):
182  item, topic_name + '[%d]' % index, type_name, slot, is_editable)
183 
184  else:
185  item.setText(self._column_index['expression'], repr(message))
186 
187  return item
188 
189  @Slot('QTreeWidgetItem*', int)
190  def request_tree_widget_itemChanged(self, item, column):
191  column_name = self.column_names[column]
192  new_value = str(item.text(column))
193  # qDebug(
194  # 'ServiceCaller.request_tree_widget_itemChanged(): %s : %s' %
195  # (column_name, new_value))
196 
197  if column_name == 'expression':
198  topic_name = str(item.data(0, Qt.UserRole))
199  self._service_info['expressions'][topic_name] = new_value
200  # qDebug(
201  # 'ServiceCaller.request_tree_widget_itemChanged(): %s expression: %s' %
202  # (topic_name, new_value))
203 
204  def fill_message_slots(self, message, topic_name, expressions, counter):
205  if not hasattr(message, '__slots__'):
206  return
207  for slot_name in message.__slots__:
208  slot_key = topic_name + '/' + slot_name
209 
210  # if no expression exists for this slot_key, continue with it's child slots
211  if slot_key not in expressions:
212  self.fill_message_slots(getattr(message, slot_name), slot_key, expressions, counter)
213  continue
214 
215  expression = expressions[slot_key]
216  if len(expression) == 0:
217  continue
218 
219  # get slot type
220  slot = getattr(message, slot_name)
221  if hasattr(slot, '_type'):
222  slot_type = slot._type
223  else:
224  slot_type = type(slot)
225 
226  self._eval_locals['i'] = counter
227  value = self._evaluate_expression(expression, slot_type)
228  if value is not None:
229  setattr(message, slot_name, value)
230 
231  def _evaluate_expression(self, expression, slot_type):
232  successful_eval = True
233  successful_conversion = True
234 
235  try:
236  # try to evaluate expression
237  value = eval(expression, {}, self._eval_locals)
238  except Exception:
239  # just use expression-string as value
240  value = expression
241  successful_eval = False
242 
243  try:
244  # try to convert value to right type
245  value = slot_type(value)
246  except Exception:
247  successful_conversion = False
248 
249  if successful_conversion:
250  return value
251  elif successful_eval:
252  qWarning(
253  'ServiceCaller.fill_message_slots(): can not convert expression to slot type: %s -> %s' %
254  (type(value), slot_type))
255  else:
256  qWarning('ServiceCaller.fill_message_slots(): failed to evaluate expression: %s' %
257  (expression))
258 
259  return None
260 
261  @Slot()
263  self.response_tree_widget.clear()
264 
265  request = self._service_info['service_class']._request_class()
266  self.fill_message_slots(request, self._service_info['service_name'],
267  self._service_info['expressions'], self._service_info['counter'])
268  try:
269  response = self._service_info['service_proxy'](request)
270  except rospy.ServiceException as e:
271  qWarning('ServiceCaller.on_call_service_button_clicked(): request:\n%r' % (request))
272  qWarning('ServiceCaller.on_call_service_button_clicked(): error calling service "%s":\n%s' %
273  (self._service_info['service_name'], e))
274  top_level_item = QTreeWidgetItem()
275  top_level_item.setText(self._column_index['service'], 'ERROR')
276  top_level_item.setText(self._column_index['type'], 'rospy.ServiceException')
277  top_level_item.setText(self._column_index['expression'], str(e))
278  else:
279  # qDebug('ServiceCaller.on_call_service_button_clicked(): response: %r' % (response))
280  top_level_item = self._recursive_create_widget_items(
281  None, '/', response._type, response, is_editable=False)
282 
283  self.response_tree_widget.addTopLevelItem(top_level_item)
284  # resize columns
285  self.response_tree_widget.expandAll()
286  for i in range(self.response_tree_widget.columnCount()):
287  self.response_tree_widget.resizeColumnToContents(i)
288 
289  @Slot('QPoint')
291  self._show_context_menu(
292  self.request_tree_widget.itemAt(pos), self.request_tree_widget.mapToGlobal(pos))
293 
294  @Slot('QPoint')
296  self._show_context_menu(
297  self.response_tree_widget.itemAt(pos), self.response_tree_widget.mapToGlobal(pos))
298 
299  def _show_context_menu(self, item, global_pos):
300  if item is None:
301  return
302 
303  # show context menu
304  menu = QMenu(self)
305  action_item_expand = menu.addAction(QIcon.fromTheme('zoom-in'), "Expand All Children")
306  action_item_collapse = menu.addAction(QIcon.fromTheme('zoom-out'), "Collapse All Children")
307  action = menu.exec_(global_pos)
308 
309  # evaluate user action
310  if action in (action_item_expand, action_item_collapse):
311  expanded = (action is action_item_expand)
312 
313  def recursive_set_expanded(item):
314  item.setExpanded(expanded)
315  for index in range(item.childCount()):
316  recursive_set_expanded(item.child(index))
317  recursive_set_expanded(item)
def save_settings(self, plugin_settings, instance_settings)
def restore_settings(self, plugin_settings, instance_settings)
def fill_message_slots(self, message, topic_name, expressions, counter)
def _recursive_create_widget_items(self, parent, topic_name, type_name, message, is_editable=True)


rqt_service_caller
Author(s): Dirk Thomas, Dorian Scholz
autogenerated on Wed Apr 21 2021 02:34:39