Merge controller into dev: bounded trajectories and shared seven-axis limits

Preserve dev TCP gizmo motion-priority handling while integrating Ruckig smoothing, continuous JTC spline validation and shared robot limits. Resolve launch and dependency conflicts without removing either feature set. Verified merged build of 8 packages and tests of 4 packages.
This commit is contained in:
Даниил Грабарь
2026-09-21 05:21:28 +03:00
41 changed files with 1333 additions and 136 deletions
+44
View File
@@ -28,6 +28,50 @@ target_compile_features(tcp_gizmo PUBLIC cxx_std_17)
target_include_directories(tcp_gizmo PRIVATE include)
ament_target_dependencies(tcp_gizmo rclcpp rclcpp_action control_msgs iiwa_msgs geometry_msgs trajectory_msgs
action_msgs moveit_servo moveit_ros_planning moveit_core tf2_eigen)
find_package(pluginlib REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
find_package(pybind11 REQUIRED)
pybind11_add_module(_iiwa_spline_limits src/spline_limits_py.cpp)
target_compile_features(_iiwa_spline_limits PRIVATE cxx_std_17)
target_include_directories(_iiwa_spline_limits PRIVATE include)
ament_get_python_install_dir(PYTHON_INSTALL_DIR)
install(TARGETS _iiwa_spline_limits LIBRARY DESTINATION ${PYTHON_INSTALL_DIR})
add_library(iiwa_spline_limits SHARED src/limit_spline_dynamics.cpp)
target_compile_features(iiwa_spline_limits PUBLIC cxx_std_17)
target_include_directories(iiwa_spline_limits PRIVATE include)
ament_target_dependencies(iiwa_spline_limits rclcpp moveit_core moveit_ros_planning pluginlib sensor_msgs)
pluginlib_export_plugin_description_file(moveit_core planning_response_plugins.xml)
install(TARGETS iiwa_spline_limits LIBRARY DESTINATION lib)
if(BUILD_TESTING)
enable_testing()
add_executable(test_spline_limits test/test_spline_limits.cpp)
target_compile_features(test_spline_limits PRIVATE cxx_std_17)
target_compile_options(test_spline_limits PRIVATE -UNDEBUG)
target_include_directories(test_spline_limits PRIVATE include)
add_test(NAME spline_limits COMMAND test_spline_limits)
find_package(joint_trajectory_controller REQUIRED)
add_executable(test_jtc_interpolation test/test_jtc_interpolation.cpp)
target_compile_features(test_jtc_interpolation PRIVATE cxx_std_17)
target_compile_options(test_jtc_interpolation PRIVATE -UNDEBUG)
target_include_directories(test_jtc_interpolation PRIVATE include)
ament_target_dependencies(test_jtc_interpolation joint_trajectory_controller)
add_test(NAME jtc_interpolation COMMAND test_jtc_interpolation)
add_executable(test_response_adapter test/test_response_adapter.cpp)
target_compile_options(test_response_adapter PRIVATE -UNDEBUG)
ament_target_dependencies(test_response_adapter moveit_core moveit_ros_planning pluginlib)
add_test(NAME response_adapter COMMAND test_response_adapter)
add_executable(test_start_state_monitor test/test_start_state_monitor.cpp)
target_compile_options(test_start_state_monitor PRIVATE -UNDEBUG)
target_include_directories(test_start_state_monitor PRIVATE include)
ament_target_dependencies(test_start_state_monitor rclcpp sensor_msgs)
add_test(NAME start_state_monitor COMMAND test_start_state_monitor)
set_tests_properties(start_state_monitor PROPERTIES ENVIRONMENT "ROS_DOMAIN_ID=231;ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST" TIMEOUT 15)
add_test(NAME direct_trajectory COMMAND ${Python3_EXECUTABLE} -m pytest -q ${CMAKE_CURRENT_SOURCE_DIR}/test/test_prepare_trajectory.py)
set_tests_properties(direct_trajectory PROPERTIES ENVIRONMENT "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}:${CMAKE_CURRENT_SOURCE_DIR}:$ENV{PYTHONPATH}")
endif()
add_executable(motion_planning_cpp src/motion_planning_cpp.cpp)
target_include_directories(motion_planning_cpp PUBLIC include)
@@ -0,0 +1,26 @@
"""Prepare position-only waypoints for the JTC quintic interpolator, off the RT thread."""
import numpy as np
from scipy.interpolate import make_interp_spline
from _iiwa_spline_limits import retime
def prepare_trajectory(times, positions, limits):
"""Preserve waypoints, fit a C2+ rest-to-rest curve, bound its complete interpolation.
`times` starts at zero. Limits contain [lower, upper, velocity, acceleration, jerk].
Returns (times, positions, velocities, accelerations, uniform_time_scale).
Timing may increase; intermediate path can differ from position-only linear interpolation.
No collision checking is performed by this direct-command API.
"""
t, q = np.asarray(times, dtype=float), np.asarray(positions, dtype=float)
if t.ndim != 1 or q.ndim != 2 or len(t) < 2 or len(t) != len(q):
raise ValueError('At least two complete trajectory points are required')
if not np.isfinite(t).all() or not np.isfinite(q).all() or t[0] != 0 or np.any(np.diff(t) < 1e-6):
raise ValueError('Finite waypoints and strictly increasing times starting at zero are required')
if q.shape[1] != len(limits):
raise ValueError('Joint count does not match limits')
zero = np.zeros(q.shape[1])
# With two derivative constraints per endpoint, knots coincide with all input times.
# Thus JTC quintics reproduce these spline segments from their endpoint p/v/a.
spline = make_interp_spline(t, q, k=5, bc_type=([(1, zero), (2, zero)], [(1, zero), (2, zero)]))
return retime(t.tolist(), q.tolist(), spline(t, 1).tolist(), spline(t, 2).tolist(), limits)
@@ -0,0 +1,147 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <stdexcept>
#include <vector>
namespace iiwa_planning {
struct Limit {
double lower, upper, velocity, acceleration, jerk;
};
struct Sample {
double time;
std::vector<double> position, velocity, acceleration;
};
// Normalized time u=t/T; exactly the quintic used for JTC
// position/velocity/acceleration points.
inline std::vector<double> quintic(double p0, double p1, double v0, double v1,
double a0, double a1, double t) {
const double d = p1 - p0, v = v0 * t, w = v1 * t, a = a0 * t * t,
b = a1 * t * t;
return {p0,
v,
a / 2,
10 * d - 6 * v - 4 * w - 1.5 * a + .5 * b,
-15 * d + 8 * v + 7 * w + 1.5 * a - b,
6 * d - 3 * v - 3 * w - .5 * a + .5 * b};
}
inline double choose(unsigned n, unsigned k) {
double x = 1;
for (unsigned i = 1; i <= k; ++i)
x = x * (n + 1 - i) / i;
return x;
}
// Convex hull bound in Bernstein basis, tightened by de Casteljau subdivision.
// Unlike sampling, it also bounds extrema between evaluation points.
inline std::pair<double, double> hull(const std::vector<double> &b,
unsigned depth) {
if (!depth)
return {*std::min_element(b.begin(), b.end()),
*std::max_element(b.begin(), b.end())};
auto v = b;
std::vector<double> left(b.size()), right(b.size());
left[0] = v[0];
right.back() = v.back();
for (size_t k = 1; k < b.size(); ++k) {
for (size_t i = 0; i < b.size() - k; ++i)
v[i] = (v[i] + v[i + 1]) / 2;
left[k] = v[0];
right[b.size() - 1 - k] = v[b.size() - 1 - k];
}
const auto l = hull(left, depth - 1), r = hull(right, depth - 1);
return {std::min(l.first, r.first), std::max(l.second, r.second)};
}
inline std::pair<double, double> bounds(const std::vector<double> &power) {
std::vector<double> b(power.size(), 0);
const unsigned n = power.size() - 1;
for (unsigned i = 0; i <= n; ++i)
for (unsigned k = 0; k <= i; ++k)
b[i] += power[k] * choose(i, k) / choose(n, k);
return hull(b, 5);
}
inline double limit_splines(std::vector<Sample> &points,
const std::vector<Limit> &limits) {
if (points.size() < 2 || limits.empty())
throw std::runtime_error("Need at least two trajectory points");
for (const auto &l : limits) {
for (double x : {l.lower, l.upper, l.velocity, l.acceleration, l.jerk})
if (!std::isfinite(x))
throw std::runtime_error("Non-finite joint limit");
if (l.lower >= l.upper || l.velocity <= 0 || l.acceleration <= 0 ||
l.jerk <= 0)
throw std::runtime_error("Invalid joint limits");
}
for (const auto &p : points) {
if (!std::isfinite(p.time) || p.time < 0 || p.time > 2147483647.0)
throw std::runtime_error("Invalid trajectory time");
for (const auto *values : {&p.position, &p.velocity, &p.acceleration}) {
if (values->size() != limits.size())
throw std::runtime_error("Incomplete trajectory derivatives");
for (double x : *values)
if (!std::isfinite(x))
throw std::runtime_error("Non-finite trajectory");
}
}
if (std::abs(points.front().time) > 1e-9)
throw std::runtime_error("Trajectory must start at t=0");
for (const auto *p : {&points.front(), &points.back()}) {
for (double v : p->velocity)
if (std::abs(v) > 1e-9)
throw std::runtime_error(
"Only stationary trajectory boundaries are supported");
for (double a : p->acceleration)
if (std::abs(a) > 1e-9)
throw std::runtime_error(
"Boundary acceleration must be zero for bounded jerk");
}
double scale = 1;
for (size_t i = 1; i < points.size(); ++i) {
const auto &p = points[i - 1];
const auto &q = points[i];
const double dt = q.time - p.time;
if (dt < 1e-6)
throw std::runtime_error(
"Trajectory times must strictly increase (>= 1 us)");
for (size_t j = 0; j < limits.size(); ++j) {
auto c = quintic(p.position[j], q.position[j], p.velocity[j],
q.velocity[j], p.acceleration[j], q.acceleration[j], dt);
auto position = bounds(c);
const auto &l = limits[j];
if (position.first < l.lower - 1e-10 || position.second > l.upper + 1e-10)
throw std::runtime_error(
"Interpolated position exceeds joint limit at joint index " +
std::to_string(j));
for (unsigned order = 1; order <= 3; ++order) {
std::vector<double> derivative(c.size() - 1);
for (size_t k = 1; k < c.size(); ++k)
derivative[k - 1] = k * c[k] / dt;
c = derivative;
const auto b = bounds(c);
const double peak = std::max(std::abs(b.first), std::abs(b.second));
const double cap =
order == 1 ? l.velocity : (order == 2 ? l.acceleration : l.jerk);
const double ratio = peak / cap;
if (!std::isfinite(ratio))
throw std::runtime_error("Unrepresentable trajectory derivatives");
scale = std::max(scale, std::pow(ratio, 1.0 / order));
}
}
}
if (scale > 1.0 + 1e-9) {
scale *= 1.000001; // Margin for conversion of timestamps to nanoseconds.
for (auto &p : points) {
p.time *= scale;
if (!std::isfinite(p.time) || p.time > 2147483647.0)
throw std::runtime_error(
"Trajectory duration cannot be represented by ROS Duration");
for (double &v : p.velocity)
v /= scale;
for (double &a : p.acceleration)
a /= scale * scale;
}
} else
scale = 1;
return scale;
}
} // namespace iiwa_planning
@@ -0,0 +1,78 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cmath>
#include <mutex>
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/joint_state.hpp>
#include <vector>
namespace iiwa_planning {
// MoveIt's CurrentStateMonitor may omit velocities (copy_dynamics=false).
// Observe the original telemetry instead of treating missing dynamics as zero.
class StartStateMonitor {
public:
void initialize(const rclcpp::Node::SharedPtr &node) {
node_ = node;
for (const auto &item : std::vector<std::pair<std::string, double>>{
{"iiwa_dynamics.stopped_velocity_tolerance", .01},
{"iiwa_dynamics.state_timeout", .5}})
if (!node->has_parameter(item.first))
node->declare_parameter(item.first, item.second);
tolerance_ = node->get_parameter("iiwa_dynamics.stopped_velocity_tolerance")
.as_double();
timeout_ = node->get_parameter("iiwa_dynamics.state_timeout").as_double();
if (!std::isfinite(tolerance_) || tolerance_ <= 0 ||
!std::isfinite(timeout_) || timeout_ <= 0)
throw std::runtime_error("Invalid start-state monitoring parameters");
subscription_ = node->create_subscription<sensor_msgs::msg::JointState>(
"/joint_states", rclcpp::SensorDataQoS(),
[this](sensor_msgs::msg::JointState::ConstSharedPtr msg) {
std::lock_guard<std::mutex> lock(mutex_);
latest_ = msg;
received_ = std::chrono::steady_clock::now();
});
}
bool stationary(const std::vector<std::string> &names,
const sensor_msgs::msg::JointState &requested) const {
// Explicit nonzero requested initial dynamics are never silently discarded.
if (!requested.velocity.empty() && !velocities_ok(names, requested))
return false;
auto node = node_.lock();
if (!node)
return velocities_ok(names,
requested); // offline use requires explicit dynamics
std::lock_guard<std::mutex> lock(mutex_);
if (!latest_ || std::chrono::duration<double>(
std::chrono::steady_clock::now() - received_)
.count() > timeout_)
return false;
const double age =
node->now().seconds() -
(latest_->header.stamp.sec + latest_->header.stamp.nanosec * 1e-9);
return std::isfinite(age) && age >= 0 && age <= timeout_ &&
velocities_ok(names, *latest_);
}
private:
bool velocities_ok(const std::vector<std::string> &names,
const sensor_msgs::msg::JointState &msg) const {
for (const auto &name : names) {
auto found = std::find(msg.name.begin(), msg.name.end(), name);
if (found == msg.name.end())
return false;
const size_t i = found - msg.name.begin();
if (i >= msg.velocity.size() || !std::isfinite(msg.velocity[i]) ||
std::abs(msg.velocity[i]) > tolerance_)
return false;
}
return true;
}
std::weak_ptr<rclcpp::Node> node_;
rclcpp::Subscription<sensor_msgs::msg::JointState>::SharedPtr subscription_;
mutable std::mutex mutex_;
sensor_msgs::msg::JointState::ConstSharedPtr latest_;
std::chrono::steady_clock::time_point received_;
double tolerance_{.01}, timeout_{.5};
};
} // namespace iiwa_planning
+6
View File
@@ -32,7 +32,13 @@
<depend>action_msgs</depend>
<depend>rcl_interfaces</depend>
<depend>tf2_eigen</depend>
<depend>pluginlib</depend>
<depend>sensor_msgs</depend>
<build_depend>pybind11-dev</build_depend>
<exec_depend>python3-numpy</exec_depend>
<test_depend>joint_trajectory_controller</test_depend>
<test_depend>python3-pytest</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_cmake_pytest</test_depend>
@@ -0,0 +1,8 @@
<library path="iiwa_spline_limits">
<class name="iiwa_planning/LimitSplineDynamics" type="iiwa_planning::LimitSplineDynamics" base_class_type="planning_interface::PlanningResponseAdapter">
<description>Bound the complete JTC quintic interpolation, stretching time without changing its joint-space curve.</description>
</class>
<class name="iiwa_planning/SetRestBoundary" type="iiwa_planning::SetRestBoundary" base_class_type="planning_interface::PlanningResponseAdapter">
<description>Zero stationary endpoint acceleration before Ruckig; reject moving endpoints.</description>
</class>
</library>
@@ -1,42 +1,112 @@
{
"home": {
"joints": [0.0, 0.0, 0.0, -1.57, 0.0, 1.57, 0.0],
"speed": 0.1
"speed": 0.5
},
"waypoints": [
{
"x": 0.6,
"y": 0.1,
"z": 0.55,
"a": 3.14,
"b": 0.3142,
"c": 2.7916,
"speed": 0.4,
"planner": "ptp"
},
{
"x": 0.6,
"y": -0.1,
"z": 0.55,
"a": 3.14,
"b": 0.3142,
"c": 2.7916,
"speed": 0.3,
"planner": "lin"
},
{
"joints": [0.5, 0.3, 0.0, -1.2, 0.0, 1.4, 0.0],
"speed": 0.2
},
{
"x": 0.58,
"y": 0.0,
"z": 0.5,
"a": 3.14,
"b": 0.3665,
"c": 2.8791,
"speed": 0.3,
"planner": "lin"
}
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.4,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.4,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.45,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.45,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.5,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.5,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.55,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.55,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.1,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.2,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.3,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.4,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.5,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.6,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.7,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.8,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":0.9,"planner":"lin"},
{"x":0.6,"y":0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"},
{"x":0.6,"y":-0.4,"z":0.5,"a":3.14,"b":0.0,"c":-3.14,"speed":1.0,"planner":"lin"}
]
}
@@ -0,0 +1,140 @@
#include "iiwa_planning/spline_limits.hpp"
#include "iiwa_planning/start_state_monitor.hpp"
#include <moveit/planning_interface/planning_response_adapter.hpp>
#include <moveit/planning_scene/planning_scene.hpp>
#include <moveit/robot_trajectory/robot_trajectory.hpp>
#include <pluginlib/class_list_macros.hpp>
namespace iiwa_planning {
// TOTG can supply nonzero endpoint acceleration. Establish rest boundary
// conditions before Ruckig, which otherwise preserves them. Positions and
// interior derivatives stay intact.
class SetRestBoundary : public planning_interface::PlanningResponseAdapter {
public:
void initialize(const rclcpp::Node::SharedPtr &node,
const std::string &) override {
start_monitor_.initialize(node);
}
std::string getDescription() const override {
return "Establish intended rest boundaries around Ruckig";
}
void adapt(const planning_scene::PlanningSceneConstPtr &,
const planning_interface::MotionPlanRequest &req,
planning_interface::MotionPlanResponse &res) const override {
if (!res.trajectory || res.trajectory->getWayPointCount() < 2 ||
!res.trajectory->getGroup()) {
res.error_code = moveit_msgs::msg::MoveItErrorCodes::INVALID_MOTION_PLAN;
return;
}
if (!start_monitor_.stationary(
res.trajectory->getGroup()->getVariableNames(),
req.start_state.joint_state)) {
RCLCPP_ERROR(
rclcpp::get_logger("iiwa_spline_limits"),
"Fresh, stationary joint_states required for rest-to-rest planning");
res.error_code = moveit_msgs::msg::MoveItErrorCodes::INVALID_ROBOT_STATE;
return;
}
for (const auto index :
{size_t(0), res.trajectory->getWayPointCount() - 1}) {
auto &state = *res.trajectory->getWayPointPtr(index);
for (const auto &name : res.trajectory->getGroup()->getVariableNames()) {
state.setVariableVelocity(name, 0.0);
state.setVariableAcceleration(name, 0.0);
}
}
}
private:
StartStateMonitor start_monitor_;
};
class LimitSplineDynamics : public planning_interface::PlanningResponseAdapter {
public:
void initialize(const rclcpp::Node::SharedPtr &node,
const std::string &) override {
start_monitor_.initialize(node);
}
std::string getDescription() const override {
return "Limit JTC quintic velocity, acceleration and jerk";
}
void adapt(const planning_scene::PlanningSceneConstPtr &,
const planning_interface::MotionPlanRequest &req,
planning_interface::MotionPlanResponse &res) const override {
if (!res.trajectory || res.trajectory->getWayPointCount() < 2) {
res.error_code = moveit_msgs::msg::MoveItErrorCodes::INVALID_MOTION_PLAN;
return;
}
try {
auto &trajectory = *res.trajectory;
const auto *group = trajectory.getGroup();
if (!group)
throw std::runtime_error("Missing trajectory group");
const auto &names = group->getVariableNames();
if (!start_monitor_.stationary(names, req.start_state.joint_state))
throw std::runtime_error(
"Fresh stationary joint_states required before spline validation");
const auto model = trajectory.getRobotModel();
auto scaling = [](double x) {
return std::isfinite(x) && x > 0 && x <= 1 ? x : 1.0;
};
std::vector<Limit> limits;
for (const auto &name : names) {
const auto &b = model->getVariableBounds(name);
if (!b.position_bounded_ || !b.velocity_bounded_ ||
!b.acceleration_bounded_ || !b.jerk_bounded_)
throw std::runtime_error(
"Missing position/velocity/acceleration/jerk bounds: " + name);
limits.push_back(
{b.min_position_, b.max_position_,
b.max_velocity_ * scaling(req.max_velocity_scaling_factor),
b.max_acceleration_ * scaling(req.max_acceleration_scaling_factor),
b.max_jerk_});
}
std::vector<Sample> samples;
double t = 0;
for (size_t i = 0; i < trajectory.getWayPointCount(); ++i) {
const auto &state = *trajectory.getWayPointPtr(i);
if (!state.hasVelocities() || !state.hasAccelerations())
throw std::runtime_error(
"JTC guard requires positions, velocities and accelerations");
t += trajectory.getWayPointDurationFromPrevious(i);
Sample s{t, {}, {}, {}};
for (const auto &name : names) {
s.position.push_back(state.getVariablePosition(name));
s.velocity.push_back(state.getVariableVelocity(name));
s.acceleration.push_back(state.getVariableAcceleration(name));
}
samples.push_back(s);
}
const double scale = limit_splines(samples, limits);
if (scale > 1) {
for (size_t i = 0; i < samples.size(); ++i) {
auto &state = *trajectory.getWayPointPtr(i);
for (size_t j = 0; j < names.size(); ++j) {
state.setVariableVelocity(names[j], samples[i].velocity[j]);
state.setVariableAcceleration(names[j], samples[i].acceleration[j]);
}
trajectory.setWayPointDurationFromPrevious(
i, trajectory.getWayPointDurationFromPrevious(i) * scale);
}
RCLCPP_INFO(
rclcpp::get_logger("iiwa_spline_limits"),
"Trajectory duration scaled by %.6f to bound JTC derivatives",
scale);
}
} catch (const std::exception &e) {
RCLCPP_ERROR(rclcpp::get_logger("iiwa_spline_limits"),
"Rejected trajectory: %s", e.what());
res.error_code = moveit_msgs::msg::MoveItErrorCodes::INVALID_MOTION_PLAN;
}
}
private:
StartStateMonitor start_monitor_;
};
} // namespace iiwa_planning
PLUGINLIB_EXPORT_CLASS(iiwa_planning::LimitSplineDynamics,
planning_interface::PlanningResponseAdapter)
PLUGINLIB_EXPORT_CLASS(iiwa_planning::SetRestBoundary,
planning_interface::PlanningResponseAdapter)
@@ -0,0 +1,30 @@
#include "iiwa_planning/spline_limits.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
PYBIND11_MODULE(_iiwa_spline_limits, m) {
m.def("retime", [](const std::vector<double> &t,
const std::vector<std::vector<double>> &q,
const std::vector<std::vector<double>> &v,
const std::vector<std::vector<double>> &a,
const std::vector<std::array<double, 5>> &caps) {
if (q.size() != t.size() || v.size() != t.size() || a.size() != t.size())
throw std::runtime_error("Point array sizes differ");
std::vector<iiwa_planning::Sample> points;
for (size_t i = 0; i < t.size(); ++i)
points.push_back({t[i], q[i], v[i], a[i]});
std::vector<iiwa_planning::Limit> limits;
for (const auto &c : caps)
limits.push_back({c[0], c[1], c[2], c[3], c[4]});
const double scale = iiwa_planning::limit_splines(points, limits);
std::vector<double> times;
std::vector<std::vector<double>> positions, velocities, accelerations;
for (const auto &p : points) {
times.push_back(p.time);
positions.push_back(p.position);
velocities.push_back(p.velocity);
accelerations.push_back(p.acceleration);
}
return py::make_tuple(times, positions, velocities, accelerations, scale);
});
}
@@ -0,0 +1,36 @@
#include "iiwa_planning/spline_limits.hpp"
#include <cassert>
#include <iostream>
#include <joint_trajectory_controller/trajectory.hpp>
int main() {
using namespace iiwa_planning;
std::vector<Sample> samples{{0, {0}, {0}, {0}}, {.2, {1}, {0}, {0}}};
std::vector<Limit> limits{{-2, 2, 1, 1, 2}};
const auto scale = limit_splines(samples, limits);
assert(scale > 1);
joint_trajectory_controller::Trajectory jtc;
trajectory_msgs::msg::JointTrajectoryPoint a, b, out;
a.positions = samples[0].position;
a.velocities = samples[0].velocity;
a.accelerations = samples[0].acceleration;
b.positions = samples[1].position;
b.velocities = samples[1].velocity;
b.accelerations = samples[1].acceleration;
const auto finish = rclcpp::Time(static_cast<int64_t>(samples[1].time * 1e9));
const auto start = rclcpp::Time(int64_t(0));
double prev = 0;
const int n = 10000;
for (int i = 0; i <= n; ++i) {
const auto now = rclcpp::Time(finish.nanoseconds() * i / n);
jtc.interpolate_between_points(start, a, finish, b, now, out);
assert(std::abs(out.velocities[0]) <= 1.000001);
assert(std::abs(out.accelerations[0]) <= 1.000001);
if (i)
assert(std::abs((out.accelerations[0] - prev) / (samples[1].time / n)) <=
2.00001);
prev = out.accelerations[0];
}
std::cout << "Installed JTC interpolation stays within "
"velocity/acceleration/jerk limits\n";
}
@@ -0,0 +1,29 @@
import numpy as np
import pytest
from iiwa_planning.direct_trajectory import prepare_trajectory
LIMITS = [[-2, 2, 1, 1, 2]]
def test_dense_points_are_smooth_and_retimed():
t, q, v, a, scale = prepare_trajectory([0, .1, .2], [[0], [.5], [1]], LIMITS)
assert scale > 1
np.testing.assert_allclose(q, [[0], [.5], [1]])
assert abs(v[1][0]) > 0 # do not stop at every CSV waypoint
np.testing.assert_allclose([v[0], v[-1], a[0], a[-1]], 0, atol=1e-9)
assert t[-1] > .2
@pytest.mark.parametrize('times,positions', [([0, 0], [[0], [1]]), ([0, 1], [[0], [float('nan')]]), ([0, 1], [[0], [3]])])
def test_bad_trajectory_is_rejected(times, positions):
with pytest.raises((ValueError, RuntimeError)):
prepare_trajectory(times, positions, LIMITS)
def test_time_scaling_preserves_quintic_between_waypoints():
from scipy.interpolate import BPoly
times, positions, velocities, accelerations, scale = prepare_trajectory([0,.1,.2], [[0],[.5],[1]], LIMITS)
curve = BPoly.from_derivatives(times, [[q,v,a] for q,v,a in zip(positions,velocities,accelerations)])
grid = np.linspace(times[0],times[-1],10001)
for order,cap in [(1,1),(2,1),(3,2)]:
assert np.max(np.abs(curve(grid,nu=order))) <= cap * 1.000001
@@ -0,0 +1,65 @@
#include <cassert>
#include <iostream>
#include <moveit/planning_interface/planning_response_adapter.hpp>
#include <moveit/robot_model/robot_model.hpp>
#include <moveit/robot_trajectory/robot_trajectory.hpp>
#include <moveit/trajectory_processing/ruckig_traj_smoothing.hpp>
#include <moveit/trajectory_processing/time_optimal_trajectory_generation.hpp>
#include <pluginlib/class_loader.hpp>
#include <srdfdom/model.h>
#include <urdf_parser/urdf_parser.h>
int main() {
auto urdf = urdf::parseURDF(
R"(<robot name="test"><link name="base"/><link name="tip"/><joint name="joint1" type="revolute"><parent link="base"/><child link="tip"/><axis xyz="0 0 1"/><limit lower="-2" upper="2" velocity="1" effort="1"/></joint></robot>)");
auto srdf = std::make_shared<srdf::Model>();
assert(srdf->initString(
*urdf,
R"(<robot name="test"><group name="arm"><joint name="joint1"/></group></robot>)"));
auto model = std::make_shared<moveit::core::RobotModel>(urdf, srdf);
auto bounds = model->getVariableBounds("joint1");
bounds.acceleration_bounded_ = bounds.jerk_bounded_ = true;
bounds.min_acceleration_ = -1;
bounds.max_acceleration_ = 1;
bounds.min_jerk_ = -2;
bounds.max_jerk_ = 2;
model->getJointModel("joint1")->setVariableBounds("joint1", bounds);
auto trajectory =
std::make_shared<robot_trajectory::RobotTrajectory>(model, "arm");
moveit::core::RobotState state(model);
state.setToDefaultValues();
state.setVariableVelocity("joint1", 0);
state.setVariableAcceleration("joint1", 0);
trajectory->addSuffixWayPoint(state, 0);
state.setVariablePosition("joint1", 1);
trajectory->addSuffixWayPoint(state, .2);
assert(trajectory_processing::TimeOptimalTrajectoryGeneration()
.computeTimeStamps(*trajectory));
pluginlib::ClassLoader<planning_interface::PlanningResponseAdapter> loader(
"moveit_core", "planning_interface::PlanningResponseAdapter");
auto guard = loader.createSharedInstance("iiwa_planning/LimitSplineDynamics");
planning_interface::MotionPlanRequest req;
req.group_name = "arm";
req.max_velocity_scaling_factor = req.max_acceleration_scaling_factor = 1;
planning_interface::MotionPlanResponse res;
res.trajectory = trajectory;
res.error_code = moveit_msgs::msg::MoveItErrorCodes::SUCCESS;
auto boundary = loader.createSharedInstance("iiwa_planning/SetRestBoundary");
req.start_state.joint_state.name = {"joint1"};
req.start_state.joint_state.position = {0};
req.start_state.joint_state.velocity = {0};
boundary->adapt(nullptr, req, res);
assert(bool(res));
assert(trajectory_processing::RuckigSmoothing::applySmoothing(*trajectory));
boundary->adapt(nullptr, req, res);
assert(bool(res));
guard->adapt(nullptr, req, res);
assert(bool(res));
assert(trajectory->getDuration() > .2);
state.setVariablePosition("joint1", 3);
trajectory->addSuffixWayPoint(state, 1);
guard->adapt(nullptr, req, res);
assert(!bool(res));
std::cout << "Installed Ruckig + pluginlib response adapter: success and "
"rejection OK\n";
}
@@ -0,0 +1,58 @@
#include "iiwa_planning/spline_limits.hpp"
#include <cassert>
#include <cmath>
#include <iostream>
using namespace iiwa_planning;
int main() {
// Rest-to-rest quintic: endpoint acceleration is zero, interior peak > 5.
std::vector<Sample> points{{0, {0}, {0}, {0}}, {1, {1}, {0}, {0}}};
std::vector<Limit> limits{{-2, 2, 2, 1, 2}};
const double scale = limit_splines(points, limits);
assert(scale > 3.0);
assert(std::abs(points.back().position[0] - 1) < 1e-12);
assert(limit_splines(points, limits) <= 1.000001);
// Consistent time scaling keeps the geometric quintic unchanged.
auto p = quintic(0, 1, 0, 0, 0, 0, 1);
auto q = quintic(0, 1, 0, 0, 0, 0, points.back().time);
for (int i = 0; i <= 5; ++i)
assert(std::abs(p[i] - q[i]) < 1e-10);
bool bad = false;
try {
std::vector<Sample> s{{0, {0}, {0}, {0}}, {0, {1}, {0}, {0}}};
limit_splines(s, limits);
} catch (const std::exception &) {
bad = true;
}
assert(bad);
bad = false;
// Positions are in range but large endpoint velocities overshoot between
// them.
try {
std::vector<Sample> s{{0, {0}, {0}, {0}},
{1, {0}, {12}, {0}},
{2, {0}, {-12}, {0}},
{3, {0}, {0}, {0}}};
limit_splines(s, limits);
} catch (const std::exception &) {
bad = true;
}
assert(bad);
bad = false;
try {
std::vector<Sample> s{{0, {NAN}, {0}, {0}}, {1, {0}, {0}, {0}}};
limit_splines(s, limits);
} catch (const std::exception &) {
bad = true;
}
assert(bad);
// Non-rest boundaries must not slip past the finite-segment jerk check.
bad = false;
try {
std::vector<Sample> s{{0, {0}, {0}, {1}}, {1, {1}, {0}, {0}}};
limit_splines(s, limits);
} catch (const std::exception &) {
bad = true;
}
assert(bad);
std::cout << "spline limits OK\n";
}
@@ -0,0 +1,41 @@
#include "iiwa_planning/start_state_monitor.hpp"
#include <cassert>
#include <iostream>
#include <thread>
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>(
"iiwa_start_monitor_test", rclcpp::NodeOptions().enable_rosout(false));
iiwa_planning::StartStateMonitor monitor;
monitor.initialize(node);
auto pub = node->create_publisher<sensor_msgs::msg::JointState>(
"/joint_states", rclcpp::SensorDataQoS());
sensor_msgs::msg::JointState
requested; // Normal MoveIt request has no velocities.
assert(!monitor.stationary({"joint1"}, requested));
sensor_msgs::msg::JointState state;
state.name = {"joint1"};
state.position = {0};
state.velocity = {0};
auto publish = [&] {
for (int i = 0; i < 30; ++i) {
state.header.stamp = node->now();
pub->publish(state);
rclcpp::spin_some(node);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
};
publish();
assert(monitor.stationary({"joint1"}, requested));
state.velocity = {.1};
publish();
assert(!monitor.stationary({"joint1"}, requested));
state.velocity = {0};
publish();
assert(monitor.stationary({"joint1"}, requested));
std::this_thread::sleep_for(std::chrono::milliseconds(550));
assert(!monitor.stationary({"joint1"}, requested));
rclcpp::shutdown();
std::cout << "Position-only MoveIt requests validated using fresh real "
"velocity feedback\n";
}