00001
00002 import importlib
00003
00004 import rospy
00005 import sensor_msgs.msg
00006 import actionlib
00007 import rostopic
00008
00009 class JoyTeleopException(Exception):
00010 pass
00011
00012
00013 class JoyTeleop:
00014 """
00015 Generic joystick teleoperation node.
00016 Will not start without configuration, has to be stored in 'teleop' parameter.
00017 See config/joy_teleop.yaml for an example.
00018 """
00019 def __init__(self):
00020 if not rospy.has_param("teleop"):
00021 rospy.logfatal("no configuration was found, taking node down")
00022 raise JoyTeleopException("no config")
00023
00024 self.publishers = {}
00025 self.al_clients = {}
00026 self.message_types = {}
00027 self.command_list = {}
00028 self.offline_actions = []
00029
00030 self.old_buttons = []
00031
00032 teleop_cfg = rospy.get_param("teleop")
00033
00034 for i in teleop_cfg:
00035 if i in self.command_list:
00036 rospy.logerr("command {} was duplicated".format(i))
00037 continue
00038 action_type = teleop_cfg[i]['type']
00039 self.add_command(i, teleop_cfg[i])
00040 if action_type == 'topic':
00041 self.register_topic(i, teleop_cfg[i])
00042 elif action_type == 'action':
00043 self.register_action(i, teleop_cfg[i])
00044 else:
00045 rospy.logerr("unknown type '%s' for command '%s'", action_type, i)
00046
00047
00048 rospy.Subscriber('joy', sensor_msgs.msg.Joy, self.joy_callback)
00049
00050
00051 rospy.Timer(rospy.Duration(2.0), self.update_actions)
00052
00053 def joy_callback(self, data):
00054 try:
00055 for c in self.command_list:
00056 if self.match_command(c, data.buttons):
00057 self.run_command(c, data)
00058 except JoyTeleopException as e:
00059 rospy.logerr("error while parsing joystick input: %s", str(e))
00060 self.old_buttons = data.buttons
00061
00062 def register_topic(self, name, command):
00063 """Add a topic publisher for a joystick command"""
00064 topic_name = command['topic_name']
00065 try:
00066 topic_type = self.get_message_type(command['message_type'])
00067 self.publishers[topic_name] = rospy.Publisher(topic_name, topic_type)
00068 except JoyTeleopException as e:
00069 rospy.logerr("could not register topic for command {}: {}".format(name, str(e)))
00070
00071 def register_action(self, name, command):
00072 """Add an action client for a joystick command"""
00073 action_name = command['action_name']
00074 try:
00075 action_type = self.get_message_type(self.get_action_type(action_name))
00076 self.al_clients[action_name] = actionlib.SimpleActionClient(action_name, action_type)
00077 if action_name in self.offline_actions:
00078 self.offline_actions.remove(action_name)
00079 except JoyTeleopException:
00080 if action_name not in self.offline_actions:
00081 self.offline_actions.append(action_name)
00082
00083 def match_command(self, c, buttons):
00084 """Find a command matching a joystick configuration"""
00085 for b in self.command_list[c]['buttons']:
00086 if b < 0 or len(buttons) <= b or buttons[b] != 1:
00087 return False
00088 return sum(buttons) == len(self.command_list[c]['buttons'])
00089
00090 def add_command(self, name, command):
00091 """Add a command to the command list"""
00092 if command['type'] == 'topic':
00093 if 'deadman_buttons' not in command:
00094 command['deadman_buttons'] = []
00095 command['buttons'] = command['deadman_buttons']
00096 elif command['type'] == 'action':
00097 if 'action_goal' not in command:
00098 command['action_goal'] = {}
00099 self.command_list[name] = command
00100
00101 def run_command(self, command, joy_state):
00102 """Run a joystick command"""
00103 cmd = self.command_list[command]
00104 if cmd['type'] == 'topic':
00105 self.run_topic(command, joy_state)
00106 elif cmd['type'] == 'action':
00107 if cmd['action_name'] in self.offline_actions:
00108 rospy.logerr("command {} was not played because the action "
00109 "server was unavailable. Trying to reconnect..."
00110 .format(cmd['action_name']))
00111 self.register_action(command, self.command_list[command])
00112 else:
00113 if joy_state.buttons != self.old_buttons:
00114 self.run_action(command, joy_state)
00115 else:
00116 raise JoyTeleopException('command {} is neither a topic publisher nor an action client'
00117 .format(command))
00118
00119 def run_topic(self, c, joy_state):
00120 cmd = self.command_list[c]
00121 msg = self.get_message_type(cmd['message_type'])()
00122 for mapping in cmd['axis_mappings']:
00123 val = joy_state.axes[mapping['axis']] * mapping.get('scale', 1.0) + mapping.get('offset', 0.0)
00124 self.set_member(msg, mapping['target'], val)
00125 self.publishers[cmd['topic_name']].publish(msg)
00126
00127 def run_action(self, c, joy_state):
00128 cmd = self.command_list[c]
00129 goal = self.get_message_type(self.get_action_type(cmd['action_name'])[:-6] + 'Goal')()
00130 self.fill_msg(goal, cmd['action_goal'])
00131 self.al_clients[cmd['action_name']].send_goal(goal)
00132
00133 def fill_msg(self, msg, values):
00134 for k, v in values.iteritems():
00135 if type(v) is dict:
00136 self.fill_msg(getattr(msg, k), v)
00137 else:
00138 setattr(msg, k, v)
00139
00140 def set_member(self, msg, member, value):
00141 ml = member.split('.')
00142 if len(ml) < 1:
00143 return
00144 target = msg
00145 for i in ml[:-1]:
00146 target = getattr(target, i)
00147 setattr(target, ml[-1], value)
00148
00149 def get_message_type(self, type_name):
00150 if type_name not in self.message_types:
00151 try:
00152 package, message = type_name.split('/')
00153 mod = importlib.import_module(package + '.msg')
00154 self.message_types[type_name] = getattr(mod, message)
00155 except ValueError:
00156 raise JoyTeleopException("message type format error")
00157 except ImportError:
00158 raise JoyTeleopException("module {} could not be loaded".format(package))
00159 except AttributeError:
00160 raise JoyTeleopException("message {} could not be loaded from module {}".format(package, message))
00161 return self.message_types[type_name]
00162
00163 def get_action_type(self, action_name):
00164 try:
00165 return rostopic._get_topic_type(rospy.resolve_name(action_name) + '/goal')[0][:-4]
00166 except TypeError:
00167 raise JoyTeleopException("could not find action {}".format(action_name))
00168
00169 def update_actions(self, evt=None):
00170 for name, cmd in self.command_list.iteritems():
00171 if cmd['type'] != 'action':
00172 continue
00173 if cmd['action_name'] in self.offline_actions:
00174 self.register_action(name, cmd)
00175
00176
00177 if __name__ == "__main__":
00178 try:
00179 rospy.init_node('joy_teleop')
00180 jt = JoyTeleop()
00181 rospy.spin()
00182 except JoyTeleopException:
00183 pass
00184 except rospy.ROSInterruptException:
00185 pass