diff --git a/README.md b/README.md
index 1edc0a1..df3ac29 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,22 @@ LC_ALL=C ros2 launch iiwa_moveit demo...
sudo pip3 install transforms3d --break-system-packages
```
+Отправка робота в точку:
+```
+ros2 action send_goal /iiwa/move_to_pose iiwa_msgs/action/MoveToPose \
+ "{x: 0.5, y: 0.0, z: 0.5, a: 3.14, b: 0, c: 0, speed: 0.1, planner: 'ptp'}"
+
+ros2 action send_goal --feedback /iiwa/move_to_joints iiwa_msgs/action/MoveToJoints \
+ "{joints: [0.0, 0.0, 0.0, -1.57, 0.0, 1.57, 0.0], speed: 0.4}"
+
+ros2 service call /iiwa/move_to_named iiwa_msgs/srv/MoveToNamedPose \
+ "{name: 'home', speed: 0.5}"
+
+ros2 service call /iiwa/move_to_named iiwa_msgs/srv/MoveToNamedPose \
+ "{name: 'work', speed: 0.3}"
+
+ros2 service call /iiwa/stop std_srvs/srv/Trigger "{}"
+```
Спавн объекта:
```bash
diff --git a/src/iiwa_bringup/launch/iiwa.launch.py b/src/iiwa_bringup/launch/iiwa.launch.py
index 4bf69e2..5848679 100644
--- a/src/iiwa_bringup/launch/iiwa.launch.py
+++ b/src/iiwa_bringup/launch/iiwa.launch.py
@@ -219,6 +219,13 @@ def _runtime_setup(context, *args, **kwargs):
moveit_configs.to_dict(),
{"robot_description": robot_description},
{"use_sim_time": use_sim_time},
+ {
+ "pose_link": settings.planning.pose_link,
+ "planning_group": settings.planning.planning_group,
+ "default_frame": settings.planning.default_frame,
+ "default_planner": settings.planning.default_planner,
+ "planning_attempts": settings.planning.planning_attempts,
+ },
],
)
diff --git a/src/iiwa_config/config/moveit/kinematics.yaml b/src/iiwa_config/config/moveit/kinematics.yaml
index 8490cd6..1a1dc56 100644
--- a/src/iiwa_config/config/moveit/kinematics.yaml
+++ b/src/iiwa_config/config/moveit/kinematics.yaml
@@ -1,4 +1,5 @@
iiwa_arm:
kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin
- kinematics_solver_search_resolution: 0.0050000000000000001
- kinematics_solver_timeout: 0.0050000000000000001
\ No newline at end of file
+ kinematics_solver_search_resolution: 0.005
+ kinematics_solver_timeout: 0.005
+ kinematics_solver_attempts: 3
\ No newline at end of file
diff --git a/src/iiwa_config/config/moveit/moveit_cpp.yaml b/src/iiwa_config/config/moveit/moveit_cpp.yaml
index 13c34c4..5bebb98 100644
--- a/src/iiwa_config/config/moveit/moveit_cpp.yaml
+++ b/src/iiwa_config/config/moveit/moveit_cpp.yaml
@@ -25,7 +25,7 @@ ompl_rrtc: # Namespace for individual plan request
max_acceleration_scaling_factor: 1.0 # Acceleration scaling parameter for the trajectory generation algorithm that is called (if configured) after the path planning
planning_time: 1.0 # Time budget for the motion plan request. If the planning problem cannot be solved within this time, an empty solution with error code is returned
-pilz_lin:
+pilz_ptp:
plan_request_params:
planning_attempts: 1
planning_pipeline: pilz_industrial_motion_planner
@@ -34,6 +34,15 @@ pilz_lin:
max_acceleration_scaling_factor: 1.0
planning_time: 0.8
+pilz_lin:
+ plan_request_params:
+ planning_attempts: 1
+ planning_pipeline: pilz_industrial_motion_planner
+ planner_id: "LIN"
+ max_velocity_scaling_factor: 1.0
+ max_acceleration_scaling_factor: 1.0
+ planning_time: 0.8
+
chomp_planner:
plan_request_params:
planning_attempts: 1
diff --git a/src/iiwa_config/config/setting.yaml b/src/iiwa_config/config/setting.yaml
index 8011918..7a16d71 100644
--- a/src/iiwa_config/config/setting.yaml
+++ b/src/iiwa_config/config/setting.yaml
@@ -33,6 +33,13 @@ controller:
moveit_cpp: pkg://iiwa_config/config/moveit/moveit_cpp.yaml
+planning:
+ pose_link: "link_ee" # TCP-линк для декартовых целей
+ planning_group: "iiwa_arm" # Группа планирования из SRDF
+ default_frame: "base_link" # Система отсчёта по умолчанию
+ default_planner: "ompl" # Планировщик по умолчанию
+ planning_attempts: 3 # Число попыток планирования
+
foxglove:
enabled: true # Запускать ли foxglove_bridge вместе с роботом
port: 8765 # WebSocket-порт, к которому подключается Foxglove Studio (по умолчанию 8765)
diff --git a/src/iiwa_msgs/CMakeLists.txt b/src/iiwa_msgs/CMakeLists.txt
index 78ce136..313fcef 100644
--- a/src/iiwa_msgs/CMakeLists.txt
+++ b/src/iiwa_msgs/CMakeLists.txt
@@ -4,10 +4,14 @@ project(iiwa_msgs)
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)
+find_package(action_msgs REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/MoveToPose.srv"
- DEPENDENCIES geometry_msgs
+ "srv/MoveToNamedPose.srv"
+ "action/MoveToPose.action"
+ "action/MoveToJoints.action"
+ DEPENDENCIES geometry_msgs action_msgs
)
ament_export_dependencies(rosidl_default_runtime)
diff --git a/src/iiwa_msgs/action/MoveToJoints.action b/src/iiwa_msgs/action/MoveToJoints.action
new file mode 100644
index 0000000..b49557c
--- /dev/null
+++ b/src/iiwa_msgs/action/MoveToJoints.action
@@ -0,0 +1,11 @@
+# Goal — 7 joint positions in radians
+float64[7] joints
+# Velocity scaling 0.0–1.0
+float32 speed
+---
+# Result
+bool success
+string message
+---
+# Feedback
+string state # "planning" | "executing"
diff --git a/src/iiwa_msgs/action/MoveToPose.action b/src/iiwa_msgs/action/MoveToPose.action
new file mode 100644
index 0000000..1d164da
--- /dev/null
+++ b/src/iiwa_msgs/action/MoveToPose.action
@@ -0,0 +1,21 @@
+# Goal
+float64 x
+float64 y
+float64 z
+# ZYX Euler in radians (A=yaw, B=pitch, C=roll — KUKA ABC convention)
+float64 a
+float64 b
+float64 c
+# Velocity scaling 0.0–1.0
+float32 speed
+# Planner: ompl | ptp | lin | circ | chomp (default: ompl)
+string planner
+# Reference frame (default: base_link)
+string frame_id
+---
+# Result
+bool success
+string message
+---
+# Feedback
+string state # "planning" | "executing"
diff --git a/src/iiwa_msgs/package.xml b/src/iiwa_msgs/package.xml
index e342def..329a366 100644
--- a/src/iiwa_msgs/package.xml
+++ b/src/iiwa_msgs/package.xml
@@ -11,6 +11,7 @@
rosidl_default_generators
geometry_msgs
+ action_msgs
rosidl_default_runtime
rosidl_interface_packages
diff --git a/src/iiwa_msgs/srv/MoveToNamedPose.srv b/src/iiwa_msgs/srv/MoveToNamedPose.srv
new file mode 100644
index 0000000..9ceb030
--- /dev/null
+++ b/src/iiwa_msgs/srv/MoveToNamedPose.srv
@@ -0,0 +1,7 @@
+# Named state from SRDF (e.g. "home", "work")
+string name
+# Velocity scaling 0.0–1.0
+float32 speed
+---
+bool success
+string message
diff --git a/src/iiwa_msgs/srv/MoveToPose.srv b/src/iiwa_msgs/srv/MoveToPose.srv
index 89a6f07..bc0aba3 100644
--- a/src/iiwa_msgs/srv/MoveToPose.srv
+++ b/src/iiwa_msgs/srv/MoveToPose.srv
@@ -1,4 +1,17 @@
-geometry_msgs/PoseStamped pose
+# Position in metres
+float64 x
+float64 y
+float64 z
+# Orientation as ZYX Euler angles in degrees (KUKA ABC convention: A=yaw, B=pitch, C=roll)
+float64 a
+float64 b
+float64 c
+# Velocity scaling factor 0–100 (%)
+float32 speed
+# Planner: "ompl" | "ptp" | "lin" | "chomp" (default: "ompl")
+string planner
+# Reference frame, defaults to "base_link" if empty
+string frame_id
---
bool success
string message
diff --git a/src/iiwa_planning/CMakeLists.txt b/src/iiwa_planning/CMakeLists.txt
index bec853f..abb62e8 100644
--- a/src/iiwa_planning/CMakeLists.txt
+++ b/src/iiwa_planning/CMakeLists.txt
@@ -37,13 +37,6 @@ install(TARGETS
DESTINATION lib/${PROJECT_NAME}
)
-install(PROGRAMS
- scripts/motion_planning_test.py
- # scripts/motion_planning.py
- DESTINATION lib/${PROJECT_NAME}
- RENAME motion_planning
-)
-
install(PROGRAMS
scripts/move_to_pose_server.py
DESTINATION lib/${PROJECT_NAME}
diff --git a/src/iiwa_planning/scripts/motion_planning.py b/src/iiwa_planning/scripts/motion_planning.py
deleted file mode 100644
index 4a4ec04..0000000
--- a/src/iiwa_planning/scripts/motion_planning.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env python3
-
-import rclpy
-from rclpy.node import Node
-from moveit.core.robot_state import RobotState
-from moveit.planning import MoveItPy, PlanningComponent
-from geometry_msgs.msg import PoseStamped
-from rclpy.executors import ExternalShutdownException
-from tf_transformations import quaternion_from_euler
-
-
-class MotionPlaning(Node):
- def __init__(self):
- super().__init__("motion_planning_node")
-
- self.moveit = MoveItPy(node_name="motion_planning_node")
- self.robot_arm: PlanningComponent = self.moveit.get_planning_component("iiwa_arm")
- self.planning_scene_monitor = self.moveit.get_planning_scene_monitor()
-
-
- self.get_logger().warn("***********************************************************")
- self.get_logger().info("MotionPlanning node is ready...")
- self.get_logger().warn("***********************************************************")
- # self.robot_arm.set_start_state(configuration_name="home")
- # self.robot_arm.set_goal_state(configuration_name="work")
- # self.plan_end_execute()
-
- self.timer = self.create_timer(5.0, self._run_once)
-
- def _run_once(self):
- self.timer.cancel()
- self.handle_plan_execute_pose(0.677, 0.25, 0.21, 3.14, 0, 3.14)
-
-
- def handle_plan_execute_pose(self, x, y, z, a, b, c):
-
- pose_goal = PoseStamped()
- pose_goal.header.frame_id="base_link"
- pose_goal.header.stamp = self.get_clock().now().to_msg()
- pose_goal.pose.position.x=x
- pose_goal.pose.position.y=y
- pose_goal.pose.position.z=z
-
- xx, yy, zz, w = quaternion_from_euler(a, b, c)
-
- pose_goal.pose.orientation.x=xx
- pose_goal.pose.orientation.y=yy
- pose_goal.pose.orientation.z=zz
- pose_goal.pose.orientation.w=w
-
- # Check collisions
- with self.planning_scene_monitor.read_only() as scene:
-
- robot_state = scene.current_state
- original_joint_positions = robot_state.get_joint_group_positions("iiwa_arm")
-
- ok_ik = robot_state.set_from_ik("iiwa_arm", pose_goal.pose, "link_ee")
- robot_state.update()
-
- if not ok_ik:
- self.get_logger().warn("***********************************************************")
- self.get_logger().error("IK failed -> abort")
- self.get_logger().warn("***********************************************************")
- return
-
- robot_collision_status = scene.is_state_colliding(
- robot_state=robot_state,
- joint_model_group_name="iiwa_arm",
- verbose=True
- )
- if robot_collision_status:
- self.get_logger().warn("***********************************************************")
- self.get_logger().error("Goal in collision -> abort")
- self.get_logger().warn("***********************************************************")
- return
-
- self.get_logger().warn("***********************************************************")
- self.get_logger().info(f"\nRobot is in collision: {robot_collision_status}\n")
- self.get_logger().warn("***********************************************************")
-
- robot_state.set_joint_group_positions(
- "iiwa_arm",
- original_joint_positions,
- )
- robot_state.update()
-
- # получить текущее состояние как изначальное для перемещения
- self.robot_arm.set_start_state_to_current_state()
- # self.robot_arm.set_start_state(configuration_name="home")
- # self.robot_arm.set_goal_state(configuration_name="work")
-
- self.robot_arm.set_goal_state(
- pose_stamped_msg=pose_goal,
- pose_link="link_ee"
- )
- self.plan_end_execute()
-
-
- def plan_end_execute(self):
- self.get_logger().warn("***********************************************************")
- self.get_logger().info("Planning trajectory")
- self.get_logger().warn("***********************************************************")
-
- plan_result = self.robot_arm.plan()
- if plan_result:
- self.get_logger().warn("***********************************************************")
- self.get_logger().info("Executing plan")
- self.get_logger().warn("***********************************************************")
- robot_trajectory = plan_result.trajectory
- self.moveit.execute(robot_trajectory, controllers=[])
- else:
- self.get_logger().warn("***********************************************************")
- self.get_logger().info("Planning failed")
- self.get_logger().warn("***********************************************************")
-
-
-def main(args=None):
- try:
- rclpy.init(args=args)
- node = MotionPlaning()
- rclpy.spin(node=node)
- node.destroy_node()
- except (KeyboardInterrupt, ExternalShutdownException):
- pass
- finally:
- rclpy.shutdown()
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/iiwa_planning/scripts/motion_planning_test.py b/src/iiwa_planning/scripts/motion_planning_test.py
deleted file mode 100644
index 21da271..0000000
--- a/src/iiwa_planning/scripts/motion_planning_test.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/env python3
-import time
-
-# generic ros libraries
-import rclpy
-from moveit.core.robot_state import RobotState
-from moveit.planning import MoveItPy
-from rclpy.impl.rcutils_logger import RcutilsLogger
-from rclpy.logging import get_logger
-
-
-def plan_and_execute(robot: MoveItPy,
- planning_component,
- logger: RcutilsLogger,
- single_plan_parameters=None,
- multi_plan_parameters=None,
- sleep_time=0.0):
- logger.info("Planning trajectory")
-
- if multi_plan_parameters is not None:
- plan_result = planning_component.plan(multi_plan_parameters=multi_plan_parameters)
- elif single_plan_parameters is not None:
- plan_result = planning_component.plan(
- single_plan_parameters=single_plan_parameters
- )
- else:
- plan_result = planning_component.plan()
-
- # execute the plan
- if plan_result:
- logger.info("Executing plan")
- robot_trajectory = plan_result.trajectory
- robot.execute(robot_trajectory, controllers=[])
- else:
- logger.error("Planning failed")
-
- time.sleep(sleep_time)
-
-
-def main():
- rclpy.init()
- logger = get_logger("moveit_py.pose_goal")
-
- iiwa = MoveItPy(node_name="moveit_py")
- iiwa_arm = iiwa.get_planning_component("iiwa_arm")
- logger.info("MoveItPy instance created")
-
- iiwa_arm.set_start_state(configuration_name="ready")
- iiwa_arm.set_goal_state(configuration_name="extended")
-
- plan_and_execute(iiwa, iiwa_arm, logger, sleep_time=3.0)
-
- robot_model = iiwa.get_robot_model()
- robot_state = RobotState(robot_model)
-
- # randomize the robot state
- robot_state.set_to_random_positions()
-
- # set plan start state to current state
- iiwa_arm.set_start_state_to_current_state()
-
- # set goal state to the initialized robot state
- logger.info("Set goal state to the initialized robot state")
- iiwa_arm.set_goal_state(robot_state=robot_state)
-
- # plan to goal
- plan_and_execute(iiwa, iiwa_arm, logger, sleep_time=3.0)
-
- # set plan start state to current state
- iiwa_arm.set_start_state_to_current_state()
-
- # set pose goal with PoseStamped message
- from geometry_msgs.msg import PoseStamped
-
- pose_goal = PoseStamped()
- pose_goal.header.frame_id = "base_link"
- pose_goal.pose.orientation.w = 1.0
- pose_goal.pose.position.x = 0.28
- pose_goal.pose.position.y = -0.2
- pose_goal.pose.position.z = 0.5
- iiwa_arm.set_goal_state(pose_stamped_msg=pose_goal,
- pose_link="patron")
-
- # plan to goal
- plan_and_execute(iiwa, iiwa_arm, logger, sleep_time=3.0)
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/iiwa_planning/scripts/move_to_pose_server.py b/src/iiwa_planning/scripts/move_to_pose_server.py
index a112e7e..efb1cfc 100644
--- a/src/iiwa_planning/scripts/move_to_pose_server.py
+++ b/src/iiwa_planning/scripts/move_to_pose_server.py
@@ -1,67 +1,290 @@
#!/usr/bin/env python3
+import threading
+
import rclpy
from rclpy.node import Node
+from rclpy.action import ActionServer, CancelResponse, GoalResponse
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.executors import MultiThreadedExecutor, ExternalShutdownException
-from moveit.planning import MoveItPy, PlanningComponent
-from iiwa_msgs.srv import MoveToPose
+
+from geometry_msgs.msg import PoseStamped
+from scipy.spatial.transform import Rotation
+from std_srvs.srv import Trigger
+
+from moveit.planning import MoveItPy, PlanningComponent, PlanRequestParameters
+from moveit.core.robot_state import RobotState
+
+from iiwa_msgs.action import MoveToPose, MoveToJoints
+from iiwa_msgs.srv import MoveToNamedPose
-POSE_LINK = "link_ee"
-PLANNING_GROUP = "iiwa_arm"
+# Таблица планировщиков: имя → (pipeline, planner_id, время планирования в секундах)
+PLANNERS = {
+ "ompl": ("ompl", "RRTConnectkConfigDefault", 10.0),
+ "ptp": ("pilz_industrial_motion_planner", "PTP", 2.0),
+ "lin": ("pilz_industrial_motion_planner", "LIN", 2.0),
+ "circ": ("pilz_industrial_motion_planner", "CIRC", 2.0),
+ "chomp": ("chomp", "", 10.0),
+}
+
+
+def _abc_to_quaternion(a: float, b: float, c: float):
+ """ZYX Euler (радианы, конвенция KUKA ABC) → (qx, qy, qz, qw)."""
+ return Rotation.from_euler("ZYX", [a, b, c], degrees=False).as_quat()
+
+
+class IiwaMotionServer(Node):
+ """Сервер управления движением манипулятора iiwa.
+
+ Предоставляет:
+ - action iiwa/move_to_pose — перемещение в декартову позу
+ - action iiwa/move_to_joints — перемещение по суставным координатам
+ - service iiwa/move_to_named — перемещение в именованную позу из SRDF
+ - service iiwa/stop — немедленная остановка движения
+ """
+
+ def __init__(self):
+ super().__init__("iiwa_motion_server")
+ self._setup_parameters()
+ self._setup_moveit()
+ self._setup_servers()
+
+ self.get_logger().info("Сервер движения iiwa запущен")
+ self.get_logger().info(f" pose_link={self._pose_link} group={self._planning_group}")
+
+ def _setup_parameters(self):
+ self.declare_parameter("pose_link", "link_ee")
+ self.declare_parameter("planning_group", "iiwa_arm")
+ self.declare_parameter("default_frame", "base_link")
+ self.declare_parameter("default_planner", "ompl")
+ self.declare_parameter("planning_attempts", 3)
+
+ self._pose_link = self.get_parameter("pose_link").value
+ self._planning_group = self.get_parameter("planning_group").value
+ self._default_frame = self.get_parameter("default_frame").value
+ self._default_planner = self.get_parameter("default_planner").value
+ self._planning_attempts = self.get_parameter("planning_attempts").value
+
+ def _setup_moveit(self):
+ self._moveit = MoveItPy(node_name="iiwa_motion_server")
+ self._arm: PlanningComponent = self._moveit.get_planning_component(self._planning_group)
+ self._robot_model = self._moveit.get_robot_model()
+
+ def _setup_servers(self):
+ cb = ReentrantCallbackGroup()
+
+ ActionServer(
+ self, MoveToPose, "iiwa/move_to_pose", self._execute_pose,
+ callback_group=cb,
+ goal_callback=lambda _: GoalResponse.ACCEPT,
+ cancel_callback=lambda _: CancelResponse.ACCEPT,
+ )
+ ActionServer(
+ self, MoveToJoints, "iiwa/move_to_joints", self._execute_joints,
+ callback_group=cb,
+ goal_callback=lambda _: GoalResponse.ACCEPT,
+ cancel_callback=lambda _: CancelResponse.ACCEPT,
+ )
+ self.create_service(MoveToNamedPose, "iiwa/move_to_named", self._handle_named, callback_group=cb)
+ self.create_service(Trigger, "iiwa/stop", self._handle_stop, callback_group=cb)
+
+ def _make_plan_params(self, pipeline: str, planner_id: str, plan_time: float, velocity_scale: float) -> PlanRequestParameters:
+ params = PlanRequestParameters(self._moveit, self._planning_group)
+ params.planning_pipeline = pipeline
+ params.planner_id = planner_id
+ params.planning_time = plan_time
+ params.planning_attempts = self._planning_attempts
+ params.max_velocity_scaling_factor = velocity_scale
+ params.max_acceleration_scaling_factor = velocity_scale
+ return params
+
+ def _plan_and_execute(self, plan_params: PlanRequestParameters, goal_handle):
+ """Планирует траекторию и выполняет её с поддержкой отмены.
+
+ Возвращает (True, msg) при успехе, (False, msg) при ошибке,
+ (None, 'canceled') если цель была отменена.
+ """
+ plan_result = self._arm.plan(single_plan_parameters=plan_params)
+ if not plan_result:
+ return False, "Планирование не удалось: поза недостижима или в столкновении"
+
+ if goal_handle.is_cancel_requested:
+ return None, "canceled"
+
+ done = threading.Event()
+ failed = threading.Event()
+
+ def do_execute():
+ try:
+ self._moveit.execute(plan_result.trajectory, controllers=[])
+ except Exception as exc:
+ self.get_logger().error(f"Ошибка выполнения траектории: {exc}")
+ failed.set()
+ finally:
+ done.set()
+
+ threading.Thread(target=do_execute, daemon=True).start()
+
+ while not done.wait(timeout=0.05):
+ if goal_handle.is_cancel_requested:
+ try:
+ self._moveit.get_trajectory_execution_manager().stop_execution()
+ except Exception:
+ pass
+ done.wait()
+ return None, "canceled"
+
+ if failed.is_set():
+ return False, "Выполнение траектории завершилось ошибкой"
+
+ return True, "Движение выполнено успешно"
+
+ def _finish_action(self, goal_handle, result, ok, msg):
+ """Устанавливает финальное состояние action goal и заполняет result."""
+ result.success = bool(ok)
+ result.message = msg
+ if ok is None:
+ goal_handle.canceled()
+ elif ok:
+ goal_handle.succeed()
+ else:
+ self.get_logger().error(msg)
+ goal_handle.abort()
+ return result
+
+ def _execute_pose(self, goal_handle):
+ req = goal_handle.request
+ feedback = MoveToPose.Feedback()
+ result = MoveToPose.Result()
+
+ velocity_scale = max(0.01, min(1.0, float(req.speed)))
+ planner_key = (req.planner or self._default_planner).lower()
+
+ if planner_key not in PLANNERS:
+ result.success = False
+ result.message = f"Неизвестный планировщик '{planner_key}'. Доступные: {', '.join(PLANNERS)}"
+ self.get_logger().error(result.message)
+ goal_handle.abort()
+ return result
+
+ pipeline, planner_id, plan_time = PLANNERS[planner_key]
+
+ pose = PoseStamped()
+ pose.header.frame_id = req.frame_id or self._default_frame
+ pose.pose.position.x = req.x
+ pose.pose.position.y = req.y
+ pose.pose.position.z = req.z
+ qx, qy, qz, qw = _abc_to_quaternion(req.a, req.b, req.c)
+ pose.pose.orientation.x = qx
+ pose.pose.orientation.y = qy
+ pose.pose.orientation.z = qz
+ pose.pose.orientation.w = qw
+
+ self.get_logger().info(
+ f"[pose] xyz=({req.x:.3f}, {req.y:.3f}, {req.z:.3f}) "
+ f"abc=({req.a:.3f}, {req.b:.3f}, {req.c:.3f}) рад "
+ f"speed={velocity_scale:.2f} planner={planner_key}"
+ )
+
+ feedback.state = "planning"
+ goal_handle.publish_feedback(feedback)
+
+ self._arm.set_start_state_to_current_state()
+ self._arm.set_goal_state(pose_stamped_msg=pose, pose_link=self._pose_link)
+
+ feedback.state = "executing"
+ goal_handle.publish_feedback(feedback)
+
+ plan_params = self._make_plan_params(pipeline, planner_id, plan_time, velocity_scale)
+ ok, msg = self._plan_and_execute(plan_params, goal_handle)
+ return self._finish_action(goal_handle, result, ok, msg)
+
+ def _execute_joints(self, goal_handle):
+ req = goal_handle.request
+ feedback = MoveToJoints.Feedback()
+ result = MoveToJoints.Result()
+
+ velocity_scale = max(0.01, min(1.0, float(req.speed)))
+ joints = list(req.joints)
+
+ self.get_logger().info(
+ f"[joints] {[f'{v:.3f}' for v in joints]} speed={velocity_scale:.2f}"
+ )
+
+ feedback.state = "planning"
+ goal_handle.publish_feedback(feedback)
+
+ # Формируем целевое состояние по суставным координатам
+ goal_state = RobotState(self._robot_model)
+ goal_state.set_joint_group_positions(self._planning_group, joints)
+ goal_state.update()
+
+ self._arm.set_start_state_to_current_state()
+ self._arm.set_goal_state(robot_state=goal_state)
+
+ feedback.state = "executing"
+ goal_handle.publish_feedback(feedback)
+
+ plan_params = self._make_plan_params(
+ "ompl", "RRTConnectkConfigDefault", 10.0, velocity_scale
+ )
+ ok, msg = self._plan_and_execute(plan_params, goal_handle)
+ return self._finish_action(goal_handle, result, ok, msg)
+
+ def _handle_named(self, request: MoveToNamedPose.Request, response: MoveToNamedPose.Response):
+ name = request.name.strip()
+ velocity_scale = max(0.01, min(1.0, float(request.speed)))
+
+ self.get_logger().info(f"[named] name='{name}' speed={velocity_scale:.2f}")
+
+ self._arm.set_start_state_to_current_state()
+ try:
+ self._arm.set_goal_state(configuration_name=name)
+ except Exception as exc:
+ response.success = False
+ response.message = f"Неизвестное состояние '{name}': {exc}"
+ self.get_logger().error(response.message)
+ return response
+
+ plan_params = self._make_plan_params(
+ "ompl", "RRTConnectkConfigDefault", 10.0, velocity_scale
+ )
+ plan_result = self._arm.plan(single_plan_parameters=plan_params)
+ if not plan_result:
+ response.success = False
+ response.message = f"Не удалось построить траекторию для '{name}'"
+ self.get_logger().error(response.message)
+ return response
+
+ self._moveit.execute(plan_result.trajectory, controllers=[])
+ response.success = True
+ response.message = f"Переместился в '{name}'"
+ self.get_logger().info(response.message)
+ return response
+
+ def _handle_stop(self, request: Trigger.Request, response: Trigger.Response):
+ try:
+ self._moveit.get_trajectory_execution_manager().stop_execution()
+ response.success = True
+ response.message = "Выполнение траектории остановлено"
+ except Exception as exc:
+ response.success = False
+ response.message = str(exc)
+ self.get_logger().info(f"[stop] {response.message}")
+ return response
def main(args=None):
- rclpy.init(args=args)
-
- # MoveItPy создаёт C++ узел с именем из лонча → читает robot_description_kinematics и т.д.
- moveit = MoveItPy(node_name="move_to_pose_server")
- arm: PlanningComponent = moveit.get_planning_component(PLANNING_GROUP)
-
- # Отдельный лёгкий узел для сервиса — другое имя, нет конфликта параметров
- node = Node("move_to_pose_service")
- logger = node.get_logger()
-
- cb_group = ReentrantCallbackGroup()
-
- def handle(request: MoveToPose.Request, response: MoveToPose.Response):
- pose = request.pose
- if not pose.header.frame_id:
- pose.header.frame_id = "base_link"
-
- arm.set_start_state_to_current_state()
- arm.set_goal_state(pose_stamped_msg=pose, pose_link=POSE_LINK)
-
- logger.info(
- f"Planning to ({pose.pose.position.x:.3f}, "
- f"{pose.pose.position.y:.3f}, {pose.pose.position.z:.3f})"
- )
- plan_result = arm.plan()
-
- if not plan_result:
- response.success = False
- response.message = "Planning failed: pose may be unreachable or in collision"
- logger.error(response.message)
- return response
-
- moveit.execute(plan_result.trajectory, controllers=[])
- response.success = True
- response.message = "Motion executed successfully"
- logger.info(response.message)
- return response
-
- node.create_service(MoveToPose, "iiwa/move_to_pose", handle, callback_group=cb_group)
- logger.info(f"MoveToPoseServer ready (pose_link={POSE_LINK})")
-
try:
+ rclpy.init(args=args)
+ node = IiwaMotionServer()
executor = MultiThreadedExecutor()
executor.add_node(node)
executor.spin()
except (KeyboardInterrupt, ExternalShutdownException):
pass
finally:
- node.destroy_node()
rclpy.shutdown()
diff --git a/src/iiwa_utils/iiwa_utils/setting_loader.py b/src/iiwa_utils/iiwa_utils/setting_loader.py
index 0ed66fc..83df5b4 100644
--- a/src/iiwa_utils/iiwa_utils/setting_loader.py
+++ b/src/iiwa_utils/iiwa_utils/setting_loader.py
@@ -54,6 +54,15 @@ class ControllerCfg:
moveit: MoveitCfg
+@dataclass(frozen=True)
+class PlanningCfg:
+ pose_link: str # TCP-линк для декартовых целей
+ planning_group: str # Группа планирования из SRDF
+ default_frame: str # Система отсчёта по умолчанию
+ default_planner: str # Планировщик по умолчанию
+ planning_attempts: int # Число попыток планирования
+
+
@dataclass(frozen=True)
class FoxgloveCfg:
enabled: bool # Запускать ли foxglove_bridge
@@ -87,6 +96,7 @@ class Settings:
robot: RobotCfg
digital_twin: DigitalTwinCfg
controller: ControllerCfg
+ planning: PlanningCfg
foxglove: FoxgloveCfg
def to_dict(self) -> Dict[str, Any]:
@@ -276,6 +286,16 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings:
moveit=moveit,
)
+ # planning
+ planning_raw = raw.get("planning", {})
+ planning = PlanningCfg(
+ pose_link=str(planning_raw.get("pose_link", "link_ee")),
+ planning_group=str(planning_raw.get("planning_group", "iiwa_arm")),
+ default_frame=str(planning_raw.get("default_frame", "base_link")),
+ default_planner=str(planning_raw.get("default_planner", "ompl")),
+ planning_attempts=int(planning_raw.get("planning_attempts", 3)),
+ )
+
# foxglove
foxglove = _parse_foxglove(raw.get("foxglove"))
@@ -283,6 +303,7 @@ def build_settings(settings_path: str, check_files: bool = True) -> Settings:
robot=robot,
digital_twin=digital_twin,
controller=controller,
+ planning=planning,
foxglove=foxglove,
)