moveit2
The MoveIt Motion Planning Framework for ROS 2.
create_ikfast_moveit_plugin.py
Go to the documentation of this file.
1 #! /usr/bin/env python3
2 from __future__ import print_function
3 
4 """
5 IKFast Plugin Generator for MoveIt
6 
7 Creates a kinematics plugin using the output of IKFast from OpenRAVE.
8 This plugin and the move_group node can be used as a general
9 kinematics service, from within the moveit planning environment, or in
10 your own ROS node.
11 
12 Author: Dave Coleman, PickNik Inc.
13  Michael Lautman, PickNik Inc.
14  Based heavily on the arm_kinematic_tools package by Jeremy Zoss, SwRI
15  and the arm_navigation plugin generator by David Butterworth, KAIST
16 
17 Date: March 2013
18 
19 """
20 """
21 Copyright (c) 2013, Jeremy Zoss, SwRI
22 All rights reserved.
23 
24 Redistribution and use in source and binary forms, with or without
25 modification, are permitted provided that the following conditions are met:
26 
27 * Redistributions of source code must retain the above copyright
28 notice, this list of conditions and the following disclaimer.
29 * Redistributions in binary form must reproduce the above copyright
30 notice, this list of conditions and the following disclaimer in the
31 documentation and/or other materials provided with the distribution.
32 * Neither the name of the Willow Garage, Inc. nor the names of its
33 contributors may be used to endorse or promote products derived from
34 this software without specific prior written permission.
35 
36 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
37 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
38 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
40 IABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
41 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
42 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
43 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
44 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
45 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
46 POSSIBILITY OF SUCH DAMAGE.
47 """
48 
49 import re
50 import os
51 import yaml
52 from lxml import etree
53 from getpass import getuser
54 import shutil
55 import argparse
56 
57 try:
58  from ament_index_python.packages import (
59  get_package_share_directory,
60  PackageNotFoundError,
61  )
62 except ImportError:
63  print(
64  "Failed to import ament_index_python. No ROS2 environment available? Trying without."
65  )
66 
67  # define stubs
68  class PackageNotFoundError(Exception):
69  pass
70 
72  raise PackageNotFoundError
73 
74 
75 # Package containing this file
76 plugin_gen_pkg = "moveit_kinematics"
77 # Allowed search modes, see SEARCH_MODE enum in template file
78 search_modes = ["OPTIMIZE_MAX_JOINT", "OPTIMIZE_FREE_JOINT"]
79 
80 
82  parser = argparse.ArgumentParser(
83  description="Generate an IKFast MoveIt kinematic plugin"
84  )
85  parser.add_argument("robot_name", help="The name of your robot")
86  parser.add_argument(
87  "planning_group_name",
88  help="The name of the planning group for which your IKFast solution was generated",
89  )
90  parser.add_argument(
91  "ikfast_plugin_pkg",
92  help="The name of the MoveIt IKFast Kinematics Plugin to be created/updated",
93  )
94  parser.add_argument(
95  "base_link_name",
96  help="The name of the base link that was used when generating your IKFast solution",
97  )
98  parser.add_argument(
99  "eef_link_name",
100  help="The name of the end effector link that was used when generating your IKFast solution",
101  )
102  parser.add_argument(
103  "ikfast_output_path",
104  help="The full path to the analytic IK solution output by IKFast",
105  )
106  parser.add_argument(
107  "--search_mode",
108  default=search_modes[0],
109  help="The search mode used to solve IK for robots with more than 6DOF",
110  )
111  parser.add_argument(
112  "--srdf_filename", help="The name of your robot. Defaults to <robot_name>.srdf"
113  )
114  parser.add_argument(
115  "--robot_name_in_srdf",
116  help="The name of your robot as defined in the srdf. Defaults to <robot_name>",
117  )
118  parser.add_argument(
119  "--moveit_config_pkg",
120  help="The robot moveit_config package. Defaults to <robot_name>_moveit_config",
121  )
122  return parser
123 
124 
126  if args.srdf_filename is None:
127  args.srdf_filename = args.robot_name + ".srdf"
128  if args.robot_name_in_srdf is None:
129  args.robot_name_in_srdf = args.robot_name
130  if args.moveit_config_pkg is None:
131  args.moveit_config_pkg = args.robot_name + "_moveit_config"
132 
133 
134 def print_args(args):
135  print("Creating IKFastKinematicsPlugin with parameters: ")
136  print(" robot_name: %s" % args.robot_name)
137  print(" base_link_name: %s" % args.base_link_name)
138  print(" eef_link_name: %s" % args.eef_link_name)
139  print(" planning_group_name: %s" % args.planning_group_name)
140  print(" ikfast_plugin_pkg: %s" % args.ikfast_plugin_pkg)
141  print(" ikfast_output_path: %s" % args.ikfast_output_path)
142  print(" search_mode: %s" % args.search_mode)
143  print(" srdf_filename: %s" % args.srdf_filename)
144  print(" robot_name_in_srdf: %s" % args.robot_name_in_srdf)
145  print(" moveit_config_pkg: %s" % args.moveit_config_pkg)
146  print("")
147 
148 
149 def update_deps(reqd_deps, req_type, e_parent):
150  curr_deps = [e.text for e in e_parent.findall(req_type)]
151  missing_deps = set(reqd_deps) - set(curr_deps)
152  for dep in missing_deps:
153  etree.SubElement(e_parent, req_type).text = dep
154 
155 
157  if not os.path.exists(args.ikfast_output_path):
158  raise Exception("Can't find IKFast source code at " + args.ikfast_output_path)
159 
160  # Detect version of IKFast used to generate solver code
161  solver_version = 0
162  with open(args.ikfast_output_path, "r") as src:
163  for line in src:
164  if line.startswith("/// ikfast version"):
165  line_search = re.search("ikfast version (.*) generated", line)
166  if line_search:
167  solver_version = int(line_search.group(1), 0) & ~0x10000000
168  break
169  print("Found source code generated by IKFast version %s" % str(solver_version))
170 
171  # Chose template depending on IKFast version
172  if solver_version >= 56:
173  setattr(args, "template_version", 61)
174  else:
175  raise Exception("This converter requires IKFast 0.5.6 or newer.")
176 
177 
178 def xmlElement(name, text=None, **attributes):
179  e = etree.Element(name, **attributes)
180  e.text = text
181  return e
182 
183 
185  parameter_dict = {
186  "ikfast_kinematics": {
187  "link_prefix": {
188  "default_value": "",
189  "type": "string",
190  "description": "prefix added to tip- and baseframe to allow different namespaces or multi-robot setups",
191  }
192  }
193  }
194  return parameter_dict
195 
196 
198  try:
199  setattr(
200  args,
201  "ikfast_plugin_pkg_path",
202  get_package_share_directory(args.ikfast_plugin_pkg),
203  )
204  except PackageNotFoundError:
205  args.ikfast_plugin_pkg_path = os.path.abspath(args.ikfast_plugin_pkg)
206  print(
207  "Failed to find package: %s. Will create it in %s."
208  % (args.ikfast_plugin_pkg, args.ikfast_plugin_pkg_path)
209  )
210  # update pkg name to basename of path
211  args.ikfast_plugin_pkg = os.path.basename(args.ikfast_plugin_pkg_path)
212 
213  src_path = args.ikfast_plugin_pkg_path + "/src/"
214  if not os.path.exists(src_path):
215  os.makedirs(src_path)
216 
217  include_path = args.ikfast_plugin_pkg_path + "/include/"
218  if not os.path.exists(include_path):
219  os.makedirs(include_path)
220 
221  # Create package.xml
222  pkg_xml_path = args.ikfast_plugin_pkg_path + "/package.xml"
223  if not os.path.exists(pkg_xml_path):
224  root = xmlElement("package", format="2")
225  root.append(xmlElement("name", text=args.ikfast_plugin_pkg))
226  root.append(xmlElement("version", text="0.0.0"))
227  root.append(
228  xmlElement("description", text="IKFast plugin for " + args.robot_name)
229  )
230  root.append(xmlElement("license", text="BSD"))
231  user_name = getuser()
232  root.append(
233  xmlElement("maintainer", email="%s@todo.todo" % user_name, text=user_name)
234  )
235  root.append(xmlElement("buildtool_depend", text="ament_cmake"))
236  export = xmlElement("export")
237  export.append(xmlElement("build_type", text="ament_cmake"))
238  root.append(export)
239  etree.ElementTree(root).write(
240  pkg_xml_path, xml_declaration=True, pretty_print=True, encoding="UTF-8"
241  )
242  print("Created package.xml at: '%s'" % pkg_xml_path)
243 
244  # Create parameter YAML in src folder
245  parameters_yaml_path = src_path + "ikfast_kinematics_parameters.yaml"
246  if not os.path.exists(parameters_yaml_path):
247  print("Create parameters.yaml at: '%s'" % parameters_yaml_path)
248  with open(parameters_yaml_path, "w") as file:
249  documents = yaml.dump(create_parameter_dict(), file)
250 
251 
253  for candidate in [os.path.dirname(__file__) + "/../templates"]:
254  if os.path.exists(candidate) and os.path.exists(candidate + "/ikfast.h"):
255  return os.path.realpath(candidate)
256  try:
257  return os.path.join(
258  get_package_share_directory(plugin_gen_pkg),
259  "ikfast_kinematics_plugin/templates",
260  )
261  except PackageNotFoundError:
262  raise Exception("Can't find package %s" % plugin_gen_pkg)
263 
264 
266  # Copy the source code generated by IKFast into our src folder
267  src_path = args.ikfast_plugin_pkg_path + "/src/"
268  solver_file_path = (
269  src_path
270  + args.robot_name
271  + "_"
272  + args.planning_group_name
273  + "_ikfast_solver.cpp"
274  )
275  if not os.path.exists(solver_file_path) or not os.path.samefile(
276  args.ikfast_output_path, solver_file_path
277  ):
278  shutil.copy2(args.ikfast_output_path, solver_file_path)
279 
280  if not os.path.exists(solver_file_path):
281  raise Exception(
282  "Failed to copy IKFast source code from '%s' to '%s'\n"
283  "Manually copy the source file generated by IKFast to this location and re-run"
284  % (args.ikfast_output_path, solver_file_path)
285  )
286  # Remember ikfast solver file for update of MoveIt package
287  args.ikfast_output_path = solver_file_path
288 
289  # Get template folder location
290  template_dir = find_template_dir()
291 
292  # namespace for the plugin
293  setattr(args, "namespace", args.robot_name + "_" + args.planning_group_name)
294  replacements = dict(
295  _ROBOT_NAME_=args.robot_name,
296  _GROUP_NAME_=args.planning_group_name,
297  _SEARCH_MODE_=args.search_mode,
298  _EEF_LINK_=args.eef_link_name,
299  _BASE_LINK_=args.base_link_name,
300  _PACKAGE_NAME_=args.ikfast_plugin_pkg,
301  _NAMESPACE_=args.namespace,
302  )
303 
304  # Copy ikfast header file
305  copy_file(
306  template_dir + "/ikfast.h",
307  args.ikfast_plugin_pkg_path + "/include/ikfast.h",
308  "ikfast header file",
309  )
310  # Create ikfast plugin template
311  copy_file(
312  template_dir
313  + "/ikfast"
314  + str(args.template_version)
315  + "_moveit_plugin_template.cpp",
316  args.ikfast_plugin_pkg_path
317  + "/src/"
318  + args.robot_name
319  + "_"
320  + args.planning_group_name
321  + "_ikfast_moveit_plugin.cpp",
322  "ikfast plugin file",
323  replacements,
324  )
325 
326  # Create plugin definition .xml file
327  ik_library_name = args.namespace + "_moveit_ikfast_plugin"
328  plugin_def = etree.Element("library", path=ik_library_name)
329  setattr(args, "plugin_name", args.namespace + "/IKFastKinematicsPlugin")
330  cl = etree.SubElement(
331  plugin_def,
332  "class",
333  name=args.plugin_name,
334  type=args.namespace + "::IKFastKinematicsPlugin",
335  base_class_type="kinematics::KinematicsBase",
336  )
337  desc = etree.SubElement(cl, "description")
338  desc.text = (
339  "IKFast{template} plugin for closed-form kinematics of {robot} {group}".format(
340  template=args.template_version,
341  robot=args.robot_name,
342  group=args.planning_group_name,
343  )
344  )
345 
346  # Write plugin definition to file
347  plugin_file_name = ik_library_name + "_description.xml"
348  plugin_file_path = args.ikfast_plugin_pkg_path + "/" + plugin_file_name
349  etree.ElementTree(plugin_def).write(
350  plugin_file_path, xml_declaration=True, pretty_print=True, encoding="UTF-8"
351  )
352  print("Created plugin definition at '%s'" % plugin_file_path)
353 
354  # Create CMakeLists file
355  replacements.update(dict(_LIBRARY_NAME_=ik_library_name))
356  copy_file(
357  template_dir + "/CMakeLists.txt",
358  args.ikfast_plugin_pkg_path + "/CMakeLists.txt",
359  "cmake file",
360  replacements,
361  )
362 
363  # Add plugin export to package manifest
364  parser = etree.XMLParser(remove_blank_text=True)
365  package_file_name = args.ikfast_plugin_pkg_path + "/package.xml"
366  package_xml = etree.parse(package_file_name, parser).getroot()
367 
368  # Make sure at least all required dependencies are in the depends lists
369  build_deps = [
370  "liblapack-dev",
371  "moveit_core",
372  "pluginlib",
373  "rclcpp",
374  "tf2_kdl",
375  "tf2_eigen",
376  ]
377  run_deps = ["liblapack-dev", "moveit_core", "pluginlib", "rclcpp"]
378 
379  update_deps(build_deps, "build_depend", package_xml)
380  update_deps(run_deps, "exec_depend", package_xml)
381 
382  # Check that plugin definition file is in the export list
383  new_export = etree.Element("moveit_core", plugin="${prefix}/" + plugin_file_name)
384 
385  export_element = package_xml.find("export")
386  if export_element is None:
387  export_element = etree.SubElement(package_xml, "export")
388 
389  found = False
390  for el in export_element.findall("moveit_core"):
391  found = etree.tostring(new_export) == etree.tostring(el)
392  if found:
393  break
394 
395  if not found:
396  export_element.append(new_export)
397 
398  # Always write the package xml file, even if there are no changes, to ensure
399  # proper encodings are used in the future (UTF-8)
400  etree.ElementTree(package_xml).write(
401  package_file_name, xml_declaration=True, pretty_print=True, encoding="UTF-8"
402  )
403  print("Wrote package.xml at '%s'" % package_file_name)
404 
405  # Create a script for easily updating the plugin in the future in case the plugin needs to be updated
406  easy_script_file_path = args.ikfast_plugin_pkg_path + "/update_ikfast_plugin.sh"
407  with open(easy_script_file_path, "w") as f:
408  f.write(
409  "search_mode="
410  + args.search_mode
411  + "\n"
412  + "srdf_filename="
413  + args.srdf_filename
414  + "\n"
415  + "robot_name_in_srdf="
416  + args.robot_name_in_srdf
417  + "\n"
418  + "moveit_config_pkg="
419  + args.moveit_config_pkg
420  + "\n"
421  + "robot_name="
422  + args.robot_name
423  + "\n"
424  + "planning_group_name="
425  + args.planning_group_name
426  + "\n"
427  + "ikfast_plugin_pkg="
428  + args.ikfast_plugin_pkg
429  + "\n"
430  + "base_link_name="
431  + args.base_link_name
432  + "\n"
433  + "eef_link_name="
434  + args.eef_link_name
435  + "\n"
436  + "ikfast_output_path="
437  + args.ikfast_output_path
438  + "\n\n"
439  + "rosrun moveit_kinematics create_ikfast_moveit_plugin.py\\\n"
440  + " --search_mode=$search_mode\\\n"
441  + " --srdf_filename=$srdf_filename\\\n"
442  + " --robot_name_in_srdf=$robot_name_in_srdf\\\n"
443  + " --moveit_config_pkg=$moveit_config_pkg\\\n"
444  + " $robot_name\\\n"
445  + " $planning_group_name\\\n"
446  + " $ikfast_plugin_pkg\\\n"
447  + " $base_link_name\\\n"
448  + " $eef_link_name\\\n"
449  + " $ikfast_output_path\n"
450  )
451 
452  print("Created update plugin script at '%s'" % easy_script_file_path)
453 
454 
456  try:
457  moveit_config_pkg_path = get_package_share_directory(args.moveit_config_pkg)
458  except PackageNotFoundError:
459  raise Exception("Failed to find package: " + args.moveit_config_pkg)
460 
461  try:
462  srdf_file_name = moveit_config_pkg_path + "/config/" + args.srdf_filename
463  srdf = etree.parse(srdf_file_name).getroot()
464  except IOError:
465  raise Exception("Failed to find SRDF file: " + srdf_file_name)
466  except etree.XMLSyntaxError as err:
467  raise Exception(
468  "Failed to parse xml in file: %s\n%s" % (srdf_file_name, err.msg)
469  )
470 
471  if args.robot_name_in_srdf != srdf.get("name"):
472  raise Exception(
473  "Robot name in srdf ('%s') doesn't match expected name ('%s')"
474  % (srdf.get("name"), args.robot_name_in_srdf)
475  )
476 
477  groups = srdf.findall("group")
478  if len(groups) < 1:
479  raise Exception("No planning groups are defined in the SRDF")
480 
481  planning_group = None
482  for group in groups:
483  if group.get("name").lower() == args.planning_group_name.lower():
484  planning_group = group
485 
486  if planning_group is None:
487  raise Exception(
488  "Planning group '%s' not defined in the SRDF. Available groups: \n%s"
489  % (
490  args.planning_group_name,
491  ", ".join([group_name.get("name") for group_name in groups]),
492  )
493  )
494 
495  # Modify kinematics.yaml file
496  kin_yaml_file_name = moveit_config_pkg_path + "/config/kinematics.yaml"
497  with open(kin_yaml_file_name, "r") as f:
498  kin_yaml_data = yaml.safe_load(f)
499 
500  kin_yaml_data[args.planning_group_name]["kinematics_solver"] = args.plugin_name
501  with open(kin_yaml_file_name, "w") as f:
502  yaml.dump(kin_yaml_data, f, default_flow_style=False)
503 
504  print("Modified kinematics.yaml at '%s'" % kin_yaml_file_name)
505 
506 
507 def copy_file(src_path, dest_path, description, replacements=None):
508  if not os.path.exists(src_path):
509  raise Exception("Can't find %s at '%s'" % (description, src_path))
510 
511  if replacements is None:
512  replacements = dict()
513 
514  with open(src_path, "r") as f:
515  content = f.read()
516 
517  # replace templates
518  for key, value in replacements.items():
519  content = re.sub(key, value, content)
520 
521  with open(dest_path, "w") as f:
522  f.write(content)
523  print("Created %s at '%s'" % (description, dest_path))
524 
525 
526 def main():
527  parser = create_parser()
528  args = parser.parse_args()
529 
530  populate_optional(args)
531  print_args(args)
535  try:
537  except Exception as e:
538  print("Failed to update MoveIt package:\n" + str(e))
539 
540 
541 if __name__ == "__main__":
542  main()
def xmlElement(name, text=None, **attributes)
def update_deps(reqd_deps, req_type, e_parent)
def copy_file(src_path, dest_path, description, replacements=None)
void print(PropagationDistanceField &pdf, int numX, int numY, int numZ)