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 python
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  # define stubs
67  class PackageNotFoundError(Exception):
68  pass
69 
71  raise PackageNotFoundError
72 
73 
74 # Package containing this file
75 plugin_gen_pkg = "moveit_kinematics"
76 # Allowed search modes, see SEARCH_MODE enum in template file
77 search_modes = ["OPTIMIZE_MAX_JOINT", "OPTIMIZE_FREE_JOINT"]
78 
79 
81  parser = argparse.ArgumentParser(
82  description="Generate an IKFast MoveIt kinematic plugin"
83  )
84  parser.add_argument("robot_name", help="The name of your robot")
85  parser.add_argument(
86  "planning_group_name",
87  help="The name of the planning group for which your IKFast solution was generated",
88  )
89  parser.add_argument(
90  "ikfast_plugin_pkg",
91  help="The name of the MoveIt IKFast Kinematics Plugin to be created/updated",
92  )
93  parser.add_argument(
94  "base_link_name",
95  help="The name of the base link that was used when generating your IKFast solution",
96  )
97  parser.add_argument(
98  "eef_link_name",
99  help="The name of the end effector link that was used when generating your IKFast solution",
100  )
101  parser.add_argument(
102  "ikfast_output_path",
103  help="The full path to the analytic IK solution output by IKFast",
104  )
105  parser.add_argument(
106  "--search_mode",
107  default=search_modes[0],
108  help="The search mode used to solve IK for robots with more than 6DOF",
109  )
110  parser.add_argument(
111  "--srdf_filename", help="The name of your robot. Defaults to <robot_name>.srdf"
112  )
113  parser.add_argument(
114  "--robot_name_in_srdf",
115  help="The name of your robot as defined in the srdf. Defaults to <robot_name>",
116  )
117  parser.add_argument(
118  "--moveit_config_pkg",
119  help="The robot moveit_config package. Defaults to <robot_name>_moveit_config",
120  )
121  return parser
122 
123 
125  if args.srdf_filename is None:
126  args.srdf_filename = args.robot_name + ".srdf"
127  if args.robot_name_in_srdf is None:
128  args.robot_name_in_srdf = args.robot_name
129  if args.moveit_config_pkg is None:
130  args.moveit_config_pkg = args.robot_name + "_moveit_config"
131 
132 
133 def print_args(args):
134  print("Creating IKFastKinematicsPlugin with parameters: ")
135  print(" robot_name: %s" % args.robot_name)
136  print(" base_link_name: %s" % args.base_link_name)
137  print(" eef_link_name: %s" % args.eef_link_name)
138  print(" planning_group_name: %s" % args.planning_group_name)
139  print(" ikfast_plugin_pkg: %s" % args.ikfast_plugin_pkg)
140  print(" ikfast_output_path: %s" % args.ikfast_output_path)
141  print(" search_mode: %s" % args.search_mode)
142  print(" srdf_filename: %s" % args.srdf_filename)
143  print(" robot_name_in_srdf: %s" % args.robot_name_in_srdf)
144  print(" moveit_config_pkg: %s" % args.moveit_config_pkg)
145  print("")
146 
147 
148 def update_deps(reqd_deps, req_type, e_parent):
149  curr_deps = [e.text for e in e_parent.findall(req_type)]
150  missing_deps = set(reqd_deps) - set(curr_deps)
151  for dep in missing_deps:
152  etree.SubElement(e_parent, req_type).text = dep
153 
154 
156  if not os.path.exists(args.ikfast_output_path):
157  raise Exception("Can't find IKFast source code at " + args.ikfast_output_path)
158 
159  # Detect version of IKFast used to generate solver code
160  solver_version = 0
161  with open(args.ikfast_output_path, "r") as src:
162  for line in src:
163  if line.startswith("/// ikfast version"):
164  line_search = re.search("ikfast version (.*) generated", line)
165  if line_search:
166  solver_version = int(line_search.group(1), 0) & ~0x10000000
167  break
168  print("Found source code generated by IKFast version %s" % str(solver_version))
169 
170  # Chose template depending on IKFast version
171  if solver_version >= 56:
172  setattr(args, "template_version", 61)
173  else:
174  raise Exception("This converter requires IKFast 0.5.6 or newer.")
175 
176 
177 def xmlElement(name, text=None, **attributes):
178  e = etree.Element(name, **attributes)
179  e.text = text
180  return e
181 
182 
184  try:
185  setattr(
186  args,
187  "ikfast_plugin_pkg_path",
188  get_package_share_directory(args.ikfast_plugin_pkg),
189  )
190  except PackageNotFoundError:
191  args.ikfast_plugin_pkg_path = os.path.abspath(args.ikfast_plugin_pkg)
192  print(
193  "Failed to find package: %s. Will create it in %s."
194  % (args.ikfast_plugin_pkg, args.ikfast_plugin_pkg_path)
195  )
196  # update pkg name to basename of path
197  args.ikfast_plugin_pkg = os.path.basename(args.ikfast_plugin_pkg_path)
198 
199  src_path = args.ikfast_plugin_pkg_path + "/src/"
200  if not os.path.exists(src_path):
201  os.makedirs(src_path)
202 
203  include_path = args.ikfast_plugin_pkg_path + "/include/"
204  if not os.path.exists(include_path):
205  os.makedirs(include_path)
206 
207  # Create package.xml
208  pkg_xml_path = args.ikfast_plugin_pkg_path + "/package.xml"
209  if not os.path.exists(pkg_xml_path):
210  root = xmlElement("package", format="2")
211  root.append(xmlElement("name", text=args.ikfast_plugin_pkg))
212  root.append(xmlElement("version", text="0.0.0"))
213  root.append(
214  xmlElement("description", text="IKFast plugin for " + args.robot_name)
215  )
216  root.append(xmlElement("license", text="BSD"))
217  user_name = getuser()
218  root.append(
219  xmlElement("maintainer", email="%s@todo.todo" % user_name, text=user_name)
220  )
221  root.append(xmlElement("buildtool_depend", text="ament_cmake"))
222  export = xmlElement("export")
223  export.append(xmlElement("build_type", text="ament_cmake"))
224  root.append(export)
225  etree.ElementTree(root).write(
226  pkg_xml_path, xml_declaration=True, pretty_print=True, encoding="UTF-8"
227  )
228  print("Created package.xml at: '%s'" % pkg_xml_path)
229 
230 
232  for candidate in [os.path.dirname(__file__) + "/../templates"]:
233  if os.path.exists(candidate) and os.path.exists(candidate + "/ikfast.h"):
234  return os.path.realpath(candidate)
235  try:
236  return os.path.join(
237  get_package_share_directory(plugin_gen_pkg),
238  "ikfast_kinematics_plugin/templates",
239  )
240  except PackageNotFoundError:
241  raise Exception("Can't find package %s" % plugin_gen_pkg)
242 
243 
245  # Copy the source code generated by IKFast into our src folder
246  src_path = args.ikfast_plugin_pkg_path + "/src/"
247  solver_file_path = (
248  src_path
249  + args.robot_name
250  + "_"
251  + args.planning_group_name
252  + "_ikfast_solver.cpp"
253  )
254  if not os.path.exists(solver_file_path) or not os.path.samefile(
255  args.ikfast_output_path, solver_file_path
256  ):
257  shutil.copy2(args.ikfast_output_path, solver_file_path)
258 
259  if not os.path.exists(solver_file_path):
260  raise Exception(
261  "Failed to copy IKFast source code from '%s' to '%s'\n"
262  "Manually copy the source file generated by IKFast to this location and re-run"
263  % (args.ikfast_output_path, solver_file_path)
264  )
265  # Remember ikfast solver file for update of MoveIt package
266  args.ikfast_output_path = solver_file_path
267 
268  # Get template folder location
269  template_dir = find_template_dir()
270 
271  # namespace for the plugin
272  setattr(args, "namespace", args.robot_name + "_" + args.planning_group_name)
273  replacements = dict(
274  _ROBOT_NAME_=args.robot_name,
275  _GROUP_NAME_=args.planning_group_name,
276  _SEARCH_MODE_=args.search_mode,
277  _EEF_LINK_=args.eef_link_name,
278  _BASE_LINK_=args.base_link_name,
279  _PACKAGE_NAME_=args.ikfast_plugin_pkg,
280  _NAMESPACE_=args.namespace,
281  )
282 
283  # Copy ikfast header file
284  copy_file(
285  template_dir + "/ikfast.h",
286  args.ikfast_plugin_pkg_path + "/include/ikfast.h",
287  "ikfast header file",
288  )
289  # Create ikfast plugin template
290  copy_file(
291  template_dir
292  + "/ikfast"
293  + str(args.template_version)
294  + "_moveit_plugin_template.cpp",
295  args.ikfast_plugin_pkg_path
296  + "/src/"
297  + args.robot_name
298  + "_"
299  + args.planning_group_name
300  + "_ikfast_moveit_plugin.cpp",
301  "ikfast plugin file",
302  replacements,
303  )
304 
305  # Create plugin definition .xml file
306  ik_library_name = args.namespace + "_moveit_ikfast_plugin"
307  plugin_def = etree.Element("library", path=ik_library_name)
308  setattr(args, "plugin_name", args.namespace + "/IKFastKinematicsPlugin")
309  cl = etree.SubElement(
310  plugin_def,
311  "class",
312  name=args.plugin_name,
313  type=args.namespace + "::IKFastKinematicsPlugin",
314  base_class_type="kinematics::KinematicsBase",
315  )
316  desc = etree.SubElement(cl, "description")
317  desc.text = (
318  "IKFast{template} plugin for closed-form kinematics of {robot} {group}".format(
319  template=args.template_version,
320  robot=args.robot_name,
321  group=args.planning_group_name,
322  )
323  )
324 
325  # Write plugin definition to file
326  plugin_file_name = ik_library_name + "_description.xml"
327  plugin_file_path = args.ikfast_plugin_pkg_path + "/" + plugin_file_name
328  etree.ElementTree(plugin_def).write(
329  plugin_file_path, xml_declaration=True, pretty_print=True, encoding="UTF-8"
330  )
331  print("Created plugin definition at '%s'" % plugin_file_path)
332 
333  # Create CMakeLists file
334  replacements.update(dict(_LIBRARY_NAME_=ik_library_name))
335  copy_file(
336  template_dir + "/CMakeLists.txt",
337  args.ikfast_plugin_pkg_path + "/CMakeLists.txt",
338  "cmake file",
339  replacements,
340  )
341 
342  # Add plugin export to package manifest
343  parser = etree.XMLParser(remove_blank_text=True)
344  package_file_name = args.ikfast_plugin_pkg_path + "/package.xml"
345  package_xml = etree.parse(package_file_name, parser).getroot()
346 
347  # Make sure at least all required dependencies are in the depends lists
348  build_deps = [
349  "liblapack-dev",
350  "moveit_core",
351  "pluginlib",
352  "rclcpp",
353  "tf2_kdl",
354  "tf2_eigen",
355  ]
356  run_deps = ["liblapack-dev", "moveit_core", "pluginlib", "rclcpp"]
357 
358  update_deps(build_deps, "build_depend", package_xml)
359  update_deps(run_deps, "exec_depend", package_xml)
360 
361  # Check that plugin definition file is in the export list
362  new_export = etree.Element("moveit_core", plugin="${prefix}/" + plugin_file_name)
363 
364  export_element = package_xml.find("export")
365  if export_element is None:
366  export_element = etree.SubElement(package_xml, "export")
367 
368  found = False
369  for el in export_element.findall("moveit_core"):
370  found = etree.tostring(new_export) == etree.tostring(el)
371  if found:
372  break
373 
374  if not found:
375  export_element.append(new_export)
376 
377  # Always write the package xml file, even if there are no changes, to ensure
378  # proper encodings are used in the future (UTF-8)
379  etree.ElementTree(package_xml).write(
380  package_file_name, xml_declaration=True, pretty_print=True, encoding="UTF-8"
381  )
382  print("Wrote package.xml at '%s'" % package_file_name)
383 
384  # Create a script for easily updating the plugin in the future in case the plugin needs to be updated
385  easy_script_file_path = args.ikfast_plugin_pkg_path + "/update_ikfast_plugin.sh"
386  with open(easy_script_file_path, "w") as f:
387  f.write(
388  "search_mode="
389  + args.search_mode
390  + "\n"
391  + "srdf_filename="
392  + args.srdf_filename
393  + "\n"
394  + "robot_name_in_srdf="
395  + args.robot_name_in_srdf
396  + "\n"
397  + "moveit_config_pkg="
398  + args.moveit_config_pkg
399  + "\n"
400  + "robot_name="
401  + args.robot_name
402  + "\n"
403  + "planning_group_name="
404  + args.planning_group_name
405  + "\n"
406  + "ikfast_plugin_pkg="
407  + args.ikfast_plugin_pkg
408  + "\n"
409  + "base_link_name="
410  + args.base_link_name
411  + "\n"
412  + "eef_link_name="
413  + args.eef_link_name
414  + "\n"
415  + "ikfast_output_path="
416  + args.ikfast_output_path
417  + "\n\n"
418  + "rosrun moveit_kinematics create_ikfast_moveit_plugin.py\\\n"
419  + " --search_mode=$search_mode\\\n"
420  + " --srdf_filename=$srdf_filename\\\n"
421  + " --robot_name_in_srdf=$robot_name_in_srdf\\\n"
422  + " --moveit_config_pkg=$moveit_config_pkg\\\n"
423  + " $robot_name\\\n"
424  + " $planning_group_name\\\n"
425  + " $ikfast_plugin_pkg\\\n"
426  + " $base_link_name\\\n"
427  + " $eef_link_name\\\n"
428  + " $ikfast_output_path\n"
429  )
430 
431  print("Created update plugin script at '%s'" % easy_script_file_path)
432 
433 
435  try:
436  moveit_config_pkg_path = get_package_share_directory(args.moveit_config_pkg)
437  except PackageNotFoundError:
438  raise Exception("Failed to find package: " + args.moveit_config_pkg)
439 
440  try:
441  srdf_file_name = moveit_config_pkg_path + "/config/" + args.srdf_filename
442  srdf = etree.parse(srdf_file_name).getroot()
443  except IOError:
444  raise Exception("Failed to find SRDF file: " + srdf_file_name)
445  except etree.XMLSyntaxError as err:
446  raise Exception(
447  "Failed to parse xml in file: %s\n%s" % (srdf_file_name, err.msg)
448  )
449 
450  if args.robot_name_in_srdf != srdf.get("name"):
451  raise Exception(
452  "Robot name in srdf ('%s') doesn't match expected name ('%s')"
453  % (srdf.get("name"), args.robot_name_in_srdf)
454  )
455 
456  groups = srdf.findall("group")
457  if len(groups) < 1:
458  raise Exception("No planning groups are defined in the SRDF")
459 
460  planning_group = None
461  for group in groups:
462  if group.get("name").lower() == args.planning_group_name.lower():
463  planning_group = group
464 
465  if planning_group is None:
466  raise Exception(
467  "Planning group '%s' not defined in the SRDF. Available groups: \n%s"
468  % (
469  args.planning_group_name,
470  ", ".join([group_name.get("name") for group_name in groups]),
471  )
472  )
473 
474  # Modify kinematics.yaml file
475  kin_yaml_file_name = moveit_config_pkg_path + "/config/kinematics.yaml"
476  with open(kin_yaml_file_name, "r") as f:
477  kin_yaml_data = yaml.safe_load(f)
478 
479  kin_yaml_data[args.planning_group_name]["kinematics_solver"] = args.plugin_name
480  with open(kin_yaml_file_name, "w") as f:
481  yaml.dump(kin_yaml_data, f, default_flow_style=False)
482 
483  print("Modified kinematics.yaml at '%s'" % kin_yaml_file_name)
484 
485 
486 def copy_file(src_path, dest_path, description, replacements=None):
487  if not os.path.exists(src_path):
488  raise Exception("Can't find %s at '%s'" % (description, src_path))
489 
490  if replacements is None:
491  replacements = dict()
492 
493  with open(src_path, "r") as f:
494  content = f.read()
495 
496  # replace templates
497  for key, value in replacements.items():
498  content = re.sub(key, value, content)
499 
500  with open(dest_path, "w") as f:
501  f.write(content)
502  print("Created %s at '%s'" % (description, dest_path))
503 
504 
505 def main():
506  parser = create_parser()
507  args = parser.parse_args()
508 
509  populate_optional(args)
510  print_args(args)
514  try:
516  except Exception as e:
517  print("Failed to update MoveIt package:\n" + str(e))
518 
519 
520 if __name__ == "__main__":
521  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)