joy_teleop.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 import importlib
3 
4 import rospy
5 import genpy.message
6 from rospy import ROSException
7 import sensor_msgs.msg
8 import actionlib
9 import rostopic
10 import rosservice
11 from threading import Thread
12 from rosservice import ROSServiceException
13 
14 class JoyTeleopException(Exception):
15  pass
16 
17 
18 class JoyTeleop:
19  """
20  Generic joystick teleoperation node.
21  Will not start without configuration, has to be stored in 'teleop' parameter.
22  See config/joy_teleop.yaml for an example.
23  """
24  def __init__(self):
25  if not rospy.has_param("teleop"):
26  rospy.logfatal("no configuration was found, taking node down")
27  raise JoyTeleopException("no config")
28 
29  self.publishers = {}
30  self.al_clients = {}
31  self.srv_clients = {}
32  self.service_types = {}
33  self.message_types = {}
34  self.command_list = {}
35  self.offline_actions = []
36  self.offline_services = []
37 
38  self.old_buttons = []
39 
40  teleop_cfg = rospy.get_param("teleop")
41 
42  for i in teleop_cfg:
43  if i in self.command_list:
44  rospy.logerr("command {} was duplicated".format(i))
45  continue
46  action_type = teleop_cfg[i]['type']
47  self.add_command(i, teleop_cfg[i])
48  if action_type == 'topic':
49  self.register_topic(i, teleop_cfg[i])
50  elif action_type == 'action':
51  self.register_action(i, teleop_cfg[i])
52  elif action_type == 'service':
53  self.register_service(i, teleop_cfg[i])
54  else:
55  rospy.logerr("unknown type '%s' for command '%s'", action_type, i)
56 
57  # Don't subscribe until everything has been initialized.
58  rospy.Subscriber('joy', sensor_msgs.msg.Joy, self.joy_callback)
59 
60  # Run a low-freq action updater
61  rospy.Timer(rospy.Duration(2.0), self.update_actions)
62 
63  def joy_callback(self, data):
64  try:
65  for c in self.command_list:
66  if self.match_command(c, data.buttons):
67  self.run_command(c, data)
68  except JoyTeleopException as e:
69  rospy.logerr("error while parsing joystick input: %s", str(e))
70  self.old_buttons = data.buttons
71 
72  def register_topic(self, name, command):
73  """Add a topic publisher for a joystick command"""
74  topic_name = command['topic_name']
75  try:
76  topic_type = self.get_message_type(command['message_type'])
77  self.publishers[topic_name] = rospy.Publisher(topic_name, topic_type, queue_size=1)
78  except JoyTeleopException as e:
79  rospy.logerr("could not register topic for command {}: {}".format(name, str(e)))
80 
81  def register_action(self, name, command):
82  """Add an action client for a joystick command"""
83  action_name = command['action_name']
84  try:
85  action_type = self.get_message_type(self.get_action_type(action_name))
86  self.al_clients[action_name] = actionlib.SimpleActionClient(action_name, action_type)
87  if action_name in self.offline_actions:
88  self.offline_actions.remove(action_name)
89  except JoyTeleopException:
90  if action_name not in self.offline_actions:
91  self.offline_actions.append(action_name)
92 
93  class AsyncServiceProxy(object):
94  def __init__(self, name, service_class, persistent):
95  try:
96  rospy.wait_for_service(name, timeout=2.0)
97  except ROSException:
98  raise JoyTeleopException("Service {} is not available".format(name))
99  self._service_proxy = rospy.ServiceProxy(name, service_class, persistent)
100  self._thread = Thread(target=self._service_proxy, args=[])
101 
102  def __del__(self):
103  # try to join our thread - no way I know of to interrupt a service
104  # request
105  if self._thread.is_alive():
106  self._thread.join(1.0)
107 
108  def __call__(self, request):
109  if self._thread.is_alive():
110  self._thread.join(0.01)
111  if self._thread.is_alive():
112  return False
113 
114  self._thread = Thread(target=self._service_proxy, args=[request])
115  self._thread.start()
116  return True
117 
118  def register_service(self, name, command):
119  """ Add an AsyncServiceProxy for a joystick command """
120  service_name = command['service_name']
121  if 'service_persistent' not in command:
122  command['service_persistent'] = False
123  service_persistent = command['service_persistent']
124  try:
125  service_type = self.get_service_type(service_name)
126  self.srv_clients[service_name] = self.AsyncServiceProxy(
127  service_name,
128  service_type,
129  service_persistent)
130 
131  if service_name in self.offline_services:
132  self.offline_services.remove(service_name)
133  except JoyTeleopException:
134  if service_name not in self.offline_services:
135  self.offline_services.append(service_name)
136 
137  def match_command(self, c, buttons):
138  """Find a command matching a joystick configuration"""
139  for b in self.command_list[c]['buttons']:
140  if b < 0 or len(buttons) <= b or buttons[b] != 1:
141  return False
142  if self.command_list[c]['allow_combinations']:
143  return sum(buttons) >= len(self.command_list[c]['buttons'])
144  else:
145  return sum(buttons) == len(self.command_list[c]['buttons'])
146 
147  def add_command(self, name, command):
148  """Add a command to the command list"""
149  if command['type'] == 'topic':
150  if 'deadman_buttons' not in command:
151  command['deadman_buttons'] = []
152  command['buttons'] = command['deadman_buttons']
153  elif command['type'] == 'action':
154  if 'action_goal' not in command:
155  command['action_goal'] = {}
156  elif command['type'] == 'service':
157  if 'service_request' not in command:
158  command['service_request'] = {}
159  command['allow_combinations'] = command.get('allow_multiple_commands', True)
160  self.command_list[name] = command
161 
162  def run_command(self, command, joy_state):
163  """Run a joystick command"""
164  cmd = self.command_list[command]
165  if cmd['type'] == 'topic':
166  self.run_topic(command, joy_state)
167  elif cmd['type'] == 'action':
168  if cmd['action_name'] in self.offline_actions:
169  rospy.logerr("command {} was not played because the action "
170  "server was unavailable. Trying to reconnect..."
171  .format(cmd['action_name']))
172  self.register_action(command, self.command_list[command])
173  else:
174  if joy_state.buttons != self.old_buttons:
175  self.run_action(command, joy_state)
176  elif cmd['type'] == 'service':
177  if cmd['service_name'] in self.offline_services:
178  rospy.logerr("command {} was not played because the service "
179  "server was unavailable. Trying to reconnect..."
180  .format(cmd['service_name']))
181  self.register_service(command, self.command_list[command])
182  else:
183  if joy_state.buttons != self.old_buttons:
184  self.run_service(command, joy_state)
185  else:
186  raise JoyTeleopException('command {} is neither a topic publisher nor an action or service client'
187  .format(command))
188 
189  def run_topic(self, c, joy_state):
190  cmd = self.command_list[c]
191  msg = self.get_message_type(cmd['message_type'])()
192 
193  if 'message_value' in cmd:
194  for param in cmd['message_value']:
195  self.set_member(msg, param['target'], param['value'])
196 
197  else:
198  for mapping in cmd['axis_mappings']:
199  if 'axis' in mapping:
200  if len(joy_state.axes) > mapping['axis']:
201  val = joy_state.axes[mapping['axis']] * mapping.get('scale', 1.0) + mapping.get('offset', 0.0)
202  else:
203  rospy.logerr('Joystick has only {} axes (indexed from 0), but #{} was referenced in config.'.format(len(joy_state.axes), mapping['axis']))
204  val = 0.0
205  elif 'button' in mapping:
206  if len(joy_state.buttons) > mapping['button']:
207  val = joy_state.buttons[mapping['button']] * mapping.get('scale', 1.0) + mapping.get('offset', 0.0)
208  else:
209  rospy.logerr('Joystick has only {} buttons (indexed from 0), but #{} was referenced in config.'.format(len(joy_state.buttons), mapping['button']))
210  val = 0.0
211  else:
212  rospy.logerr('No Supported axis_mappings type found in: {}'.format(mapping))
213  val = 0.0
214 
215  self.set_member(msg, mapping['target'], val)
216 
217  # If there is a stamp field, fill it with rospy.Time.now()
218  if hasattr(msg, 'header'):
219  msg.header.stamp = rospy.Time.now()
220 
221  self.publishers[cmd['topic_name']].publish(msg)
222 
223  def run_action(self, c, joy_state):
224  cmd = self.command_list[c]
225  goal = self.get_message_type(self.get_action_type(cmd['action_name'])[:-6] + 'Goal')()
226  genpy.message.fill_message_args(goal, [cmd['action_goal']])
227  self.al_clients[cmd['action_name']].send_goal(goal)
228 
229  def run_service(self, c, joy_state):
230  cmd = self.command_list[c]
231  request = self.get_service_type(cmd['service_name'])._request_class()
232  # should work for requests, too
233  genpy.message.fill_message_args(request, [cmd['service_request']])
234  if not self.srv_clients[cmd['service_name']](request):
235  rospy.loginfo('Not sending new service request for command {} because previous request has not finished'
236  .format(c))
237 
238  def set_member(self, msg, member, value):
239  ml = member.split('.')
240  if len(ml) < 1:
241  return
242  target = msg
243  for i in ml[:-1]:
244  target = getattr(target, i)
245  setattr(target, ml[-1], value)
246 
247  def get_message_type(self, type_name):
248  if type_name not in self.message_types:
249  try:
250  package, message = type_name.split('/')
251  mod = importlib.import_module(package + '.msg')
252  self.message_types[type_name] = getattr(mod, message)
253  except ValueError:
254  raise JoyTeleopException("message type format error")
255  except ImportError:
256  raise JoyTeleopException("module {} could not be loaded".format(package))
257  except AttributeError:
258  raise JoyTeleopException("message {} could not be loaded from module {}".format(package, message))
259  return self.message_types[type_name]
260 
261  def get_action_type(self, action_name):
262  try:
263  return rostopic._get_topic_type(rospy.resolve_name(action_name) + '/goal')[0][:-4]
264  except TypeError:
265  raise JoyTeleopException("could not find action {}".format(action_name))
266 
267  def get_service_type(self, service_name):
268  if service_name not in self.service_types:
269  try:
270  self.service_types[service_name] = rosservice.get_service_class_by_name(service_name)
271  except ROSServiceException as e:
272  raise JoyTeleopException("service {} could not be loaded: {}".format(service_name, str(e)))
273  return self.service_types[service_name]
274 
275  def update_actions(self, evt=None):
276  for name, cmd in self.command_list.items():
277  if cmd['type'] != 'action':
278  continue
279  if cmd['action_name'] in self.offline_actions:
280  self.register_action(name, cmd)
281 
282 
283 if __name__ == "__main__":
284  try:
285  rospy.init_node('joy_teleop')
286  jt = JoyTeleop()
287  rospy.spin()
288  except JoyTeleopException:
289  pass
290  except rospy.ROSInterruptException:
291  pass
def run_service(self, c, joy_state)
Definition: joy_teleop.py:229
def register_topic(self, name, command)
Definition: joy_teleop.py:72
def get_message_type(self, type_name)
Definition: joy_teleop.py:247
def run_topic(self, c, joy_state)
Definition: joy_teleop.py:189
def get_action_type(self, action_name)
Definition: joy_teleop.py:261
def __init__(self)
Definition: joy_teleop.py:24
def update_actions(self, evt=None)
Definition: joy_teleop.py:275
def get_service_type(self, service_name)
Definition: joy_teleop.py:267
def set_member(self, msg, member, value)
Definition: joy_teleop.py:238
def joy_callback(self, data)
Definition: joy_teleop.py:63
def __init__(self, name, service_class, persistent)
Definition: joy_teleop.py:94
def run_command(self, command, joy_state)
Definition: joy_teleop.py:162
def run_action(self, c, joy_state)
Definition: joy_teleop.py:223
def add_command(self, name, command)
Definition: joy_teleop.py:147
def register_action(self, name, command)
Definition: joy_teleop.py:81
def match_command(self, c, buttons)
Definition: joy_teleop.py:137
def register_service(self, name, command)
Definition: joy_teleop.py:118


joy_teleop
Author(s): Paul Mathieu
autogenerated on Sun May 7 2023 02:17:53