Files
lightweight-cobot/src/iiwa_planning/src/tcp_gizmo.cpp
T
Даниил Грабарь 3a5bc28bba feat: add release-to-move TCP gizmo control in Webots
Add a native Webots marker for setting the active tool's TCP position and
orientation. Dragging edits the preview; releasing the mouse submits one
complete, checked joint trajectory with configured TCP speed limits.
Unreachable targets reset the marker to the current TCP.

Preserve coordinate, joint, and control-file commands through motion
reservations and a controller action proxy. Give the gizmo lowest priority
and finish its committed trajectory before dispatching queued regular
commands, including waits longer than the former two-second service timeout.

Place the patron TCP at its working end, 55.2 mm along local Z, and use the
flange TCP when no tool is attached. Add digital_twin.tcp_gizmo configuration,
ROS interfaces, launch integration, and regression coverage.

Document operation, configuration, tool attachment, speed limits, and
troubleshooting in both Russian and English.

Validation: 42 Python tests and 8 C++ tests passed. Webots regressions covered
one action per release, translation and rotation with and without a tool,
unreachable targets, and ordinary commands waiting for gizmo completion.
2026-09-17 17:11:53 +03:00

472 lines
24 KiB
C++

#include <iiwa_planning/tcp_gizmo_policy.hpp>
#include <iiwa_msgs/msg/gizmo_command.hpp>
#include <iiwa_msgs/msg/gizmo_state.hpp>
#include <iiwa_msgs/srv/motion_priority.hpp>
#include <control_msgs/action/follow_joint_trajectory.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <moveit_servo/servo.hpp>
#include <moveit_servo/utils/common.hpp>
#include <moveit/planning_scene_monitor/planning_scene_monitor.hpp>
#include <rclcpp/rclcpp.hpp>
#include <trajectory_msgs/msg/joint_trajectory.hpp>
#include <Eigen/Geometry>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <memory>
#include <deque>
#include <functional>
#include <map>
#include <stdexcept>
namespace iiwa_planning {
class TcpGizmo {
using Trajectory = trajectory_msgs::msg::JointTrajectory;
using Priority = iiwa_msgs::srv::MotionPriority;
using Follow = control_msgs::action::FollowJointTrajectory;
using ClientGoal = rclcpp_action::ClientGoalHandle<Follow>;
using ServerGoal = rclcpp_action::ServerGoalHandle<Follow>;
struct ProxyGoal {
std::shared_ptr<ServerGoal> upstream;
ClientGoal::SharedPtr downstream;
bool cancel_requested{false};
bool cancel_sent{false};
bool finished{false};
};
public:
explicit TcpGizmo(const rclcpp::Node::SharedPtr& node) : node_(node) {
if (!param<bool>("enabled", false) || !param<bool>("simulation", false))
throw std::runtime_error("TCP gizmo requires enabled=true and simulation=true");
group_name_ = param<std::string>("planning_group", "iiwa_arm");
tcp_ = param<std::string>("pose_link", "tcp");
frame_ = param<std::string>("default_frame", "base_link");
period_ = positive("publish_period", .032);
command_timeout_ = positive("command_timeout", .3);
state_timeout_ = positive("state_timeout", .5);
linear_speed_ = positive("max_linear_speed", .10);
angular_speed_ = positive("max_angular_speed", .4);
listener_ = std::make_shared<servo::ParamListener>(node_, "moveit_servo");
const auto params = listener_->get_params();
if (params.move_group_name != group_name_ || std::abs(params.publish_period - period_) > 1e-9)
throw std::runtime_error("Servo group/period must match TCP gizmo parameters");
scene_ = moveit_servo::createPlanningSceneMonitor(node_, params);
if (!scene_ || !scene_->getRobotModel()) throw std::runtime_error("Missing planning scene");
group_ = scene_->getRobotModel()->getJointModelGroup(group_name_);
if (!group_ || !group_->getSolverInstance()) throw std::runtime_error("Missing IK solver");
tip_ = group_->getSolverInstance()->getTipFrame();
const auto model = scene_->getRobotModel();
if (!model->hasLinkModel(tcp_) || !model->hasLinkModel(frame_) || !model->hasLinkModel(tip_))
throw std::runtime_error("Unknown TCP, frame or solver tip");
if (moveit::core::RobotModel::getRigidlyConnectedParentLinkModel(model->getLinkModel(tcp_)) !=
moveit::core::RobotModel::getRigidlyConnectedParentLinkModel(model->getLinkModel(tip_)))
throw std::runtime_error("TCP must be rigidly attached to the IK solver tip");
output_ = node_->create_publisher<Trajectory>("/cobot/tcp_gizmo/controller_command", 10);
state_pub_ = node_->create_publisher<iiwa_msgs::msg::GizmoState>("/cobot/tcp_gizmo/state", 1);
command_sub_ = node_->create_subscription<iiwa_msgs::msg::GizmoCommand>(
"/cobot/tcp_gizmo/command", 1, [this](const iiwa_msgs::msg::GizmoCommand& msg) {
try { command(msg); }
catch (const std::exception& e) { reject(std::string("target error: ") + e.what()); }
});
action_client_ = rclcpp_action::create_client<Follow>(node_, "/cobot/tcp_gizmo/follow_joint_trajectory");
action_server_ = rclcpp_action::create_server<Follow>(node_, "/iiwa_arm_controller/follow_joint_trajectory",
[this](const rclcpp_action::GoalUUID&, std::shared_ptr<const Follow::Goal>) {
if (!action_client_->action_server_is_ready()) return rclcpp_action::GoalResponse::REJECT;
++high_goals_count_;
gate_.action(true); // Block low before accepting a public high goal.
reason_ = "trajectory action";
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
},
[this](const std::shared_ptr<ServerGoal> goal) {
const auto it = high_goals_.find(goal->get_goal_id());
if (it != high_goals_.end()) {
it->second->cancel_requested = true;
cancelHigh(it->second);
}
return rclcpp_action::CancelResponse::ACCEPT;
},
[this](const std::shared_ptr<ServerGoal> goal) {
auto proxy = std::make_shared<ProxyGoal>();
proxy->upstream = goal;
high_goals_[goal->get_goal_id()] = proxy;
after_low_.push_back([this, proxy]() { sendHigh(proxy); });
flushHigh();
});
normal_sub_ = node_->create_subscription<Trajectory>("/iiwa_arm_controller/joint_trajectory", 10,
[this](const Trajectory& msg) {
// The low action must have a terminal controller result before ANY high
// output. DDS publication/cancel acknowledgements alone are insufficient.
gate_.normal(trajectoryDeadline(msg));
reason_ = "ordinary trajectory";
after_low_.push_back([this, msg]() {
gate_.normal(trajectoryDeadline(msg));
output_->publish(msg);
});
flushHigh();
});
priority_ = node_->create_service<Priority>("/cobot/tcp_gizmo/priority",
[this](const std::shared_ptr<rmw_request_id_t> header, const Priority::Request::SharedPtr request) {
const auto token = ++lease_requests_[request->owner];
if (!request->acquire) {
gate_.release(request->owner);
replyPriority(header, true, "priority released");
return;
}
if (!gate_.acquire(request->owner, request->ttl, steady())) {
replyPriority(header, false, "owner and finite TTL in (0, 60] required");
return;
}
reason_ = "waiting for low action to stop";
after_low_.push_back([this, header, request, token]() {
if (lease_requests_[request->owner] != token) {
replyPriority(header, false, "priority request superseded"); return;
}
// TTL starts at acknowledgement, even if controller completion was
// delayed. The pending callback itself prevents any new low command.
gate_.acquire(request->owner, request->ttl, steady());
replyPriority(header, true, "controller low action stopped; priority granted");
reason_ = "motion priority lease";
});
flushHigh();
});
timer_ = node_->create_wall_timer(std::chrono::duration<double>(period_), [this]() { tick(); });
}
private:
template<class T> T param(const std::string& name, const T& value) {
if (!node_->has_parameter(name)) return node_->declare_parameter<T>(name, value);
return node_->get_parameter(name).get_value<T>();
}
double positive(const std::string& name, double value) {
value = param<double>(name, value);
if (!std::isfinite(value) || value <= 0.) throw std::runtime_error("Invalid " + name);
return value;
}
static double steady() {
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
moveit::core::RobotStatePtr current() {
auto monitor = scene_->getStateMonitor();
if (!monitor || !monitor->haveCompleteState(rclcpp::Duration::from_seconds(state_timeout_))) return {};
auto pair = monitor->getCurrentStateAndTime();
const double age = (node_->now() - pair.second).seconds();
if (age < -.01 || age > state_timeout_) return {};
const auto stamp = pair.second.nanoseconds();
if (stamp != feedback_stamp_) { feedback_stamp_ = stamp; feedback_received_ = steady(); }
if (steady() - feedback_received_ > state_timeout_) return {};
pair.first->update();
std::vector<double> positions;
pair.first->copyJointGroupPositions(group_, positions);
if (!std::all_of(positions.begin(), positions.end(), [](double x) { return std::isfinite(x); })) return {};
return pair.first;
}
bool settled(const moveit::core::RobotState& state) {
std::vector<double> positions;
state.copyJointGroupPositions(group_, positions);
const double now = node_->now().seconds();
if (previous_.size() != positions.size() || now < previous_time_) {
previous_ = positions; previous_time_ = now; stable_since_ = now; return false;
}
if (now > previous_time_) {
double delta = 0.;
for (std::size_t i = 0; i < positions.size(); ++i) delta = std::max(delta, std::abs(positions[i] - previous_[i]));
if (delta / (now - previous_time_) > .01) stable_since_ = now;
previous_ = positions; previous_time_ = now;
}
return now - stable_since_ >= .15;
}
double trajectoryDeadline(const Trajectory& msg) const {
double start = node_->now().seconds();
if (rclcpp::Time(msg.header.stamp).nanoseconds() != 0)
start = std::max(start, rclcpp::Time(msg.header.stamp).seconds());
double duration = 0.;
for (const auto& point : msg.points)
duration = std::max(duration, rclcpp::Duration(point.time_from_start).seconds());
return start + duration;
}
void replyPriority(const std::shared_ptr<rmw_request_id_t>& header, bool success, const std::string& message) {
Priority::Response response;
response.success = success; response.message = message;
priority_->send_response(*header, response);
}
void flushHigh() {
if (!low_barrier_.idle()) return;
while (!after_low_.empty()) {
auto callback = std::move(after_low_.front());
after_low_.pop_front();
callback();
}
}
void sendLow(const Trajectory& trajectory) {
if (!action_client_->action_server_is_ready()) { reject("controller action unavailable"); return; }
if (!low_barrier_.begin()) return;
Follow::Goal goal;
goal.trajectory = trajectory;
// One released marker pose is one complete trajectory. The user chose
// to finish this trajectory before dispatching a queued ordinary command.
// No low CancelGoal request is needed (including the JTC cancel/result race).
goal.goal_time_tolerance = rclcpp::Duration::from_seconds(state_timeout_);
rclcpp_action::Client<Follow>::SendGoalOptions options;
options.goal_response_callback = [this](const ClientGoal::SharedPtr handle) {
low_goal_ = handle;
low_barrier_.accepted(static_cast<bool>(handle));
if (!handle) {
gate_.invalidate(); reason_ = "controller rejected low trajectory"; flushHigh();
}
};
options.result_callback = [this](const ClientGoal::WrappedResult& result) {
low_goal_.reset(); low_barrier_.terminal();
// A new epoch acknowledges completion to the marker and permits a new
// gesture. Never redispatch the same goal after a successful result.
gate_.invalidate();
reason_ = result.code == rclcpp_action::ResultCode::SUCCEEDED ?
"target reached" : "controller did not complete TCP trajectory";
flushHigh();
};
try { action_client_->async_send_goal(goal, options); }
catch (const std::exception& error) {
// An exception leaves delivery uncertain. Fail closed: do not forward a
// high command without a terminal controller result for this send.
gate_.invalidate(); reason_ = std::string("low action send uncertain: ") + error.what();
}
}
void cancelHigh(const std::shared_ptr<ProxyGoal>& proxy) {
if (proxy->downstream && proxy->cancel_requested && !proxy->cancel_sent && !proxy->finished) {
proxy->cancel_sent = true;
action_client_->async_cancel_goal(proxy->downstream);
}
}
void finishHigh(const std::shared_ptr<ProxyGoal>& proxy, rclcpp_action::ResultCode code,
const std::shared_ptr<Follow::Result>& result) {
if (proxy->finished) return;
proxy->finished = true;
if (proxy->upstream->is_canceling()) proxy->upstream->canceled(result);
else if (code == rclcpp_action::ResultCode::SUCCEEDED) proxy->upstream->succeed(result);
else proxy->upstream->abort(result);
high_goals_.erase(proxy->upstream->get_goal_id());
if (high_goals_count_ > 0) --high_goals_count_;
gate_.action(high_goals_count_ > 0);
}
void sendHigh(const std::shared_ptr<ProxyGoal>& proxy) {
if (proxy->cancel_requested || proxy->upstream->is_canceling()) {
auto result = std::make_shared<Follow::Result>();
result->error_string = "canceled before controller dispatch";
finishHigh(proxy, rclcpp_action::ResultCode::CANCELED, result); return;
}
if (!action_client_->action_server_is_ready()) {
auto result = std::make_shared<Follow::Result>();
result->error_code = Follow::Result::INVALID_GOAL;
result->error_string = "controller action unavailable";
finishHigh(proxy, rclcpp_action::ResultCode::ABORTED, result); return;
}
rclcpp_action::Client<Follow>::SendGoalOptions options;
options.goal_response_callback = [this, proxy](const ClientGoal::SharedPtr handle) {
proxy->downstream = handle;
if (!handle) {
auto result = std::make_shared<Follow::Result>();
result->error_code = Follow::Result::INVALID_GOAL;
result->error_string = "controller rejected trajectory";
finishHigh(proxy, rclcpp_action::ResultCode::ABORTED, result);
} else cancelHigh(proxy);
};
options.feedback_callback = [proxy](ClientGoal::SharedPtr, const std::shared_ptr<const Follow::Feedback> feedback) {
if (!proxy->finished && proxy->upstream->is_active()) proxy->upstream->publish_feedback(std::make_shared<Follow::Feedback>(*feedback));
};
options.result_callback = [this, proxy](const ClientGoal::WrappedResult& result) {
finishHigh(proxy, result.code, result.result);
};
try { action_client_->async_send_goal(*proxy->upstream->get_goal(), options); }
catch (const std::exception& error) {
// Keep its high reservation on ambiguous delivery; exposing low motion
// here could overwrite a goal that the controller actually received.
reason_ = std::string("high action send uncertain: ") + error.what();
}
}
void reject(const std::string& reason) {
RCLCPP_WARN(node_->get_logger(), "TCP gizmo rejected target: %s", reason.c_str());
gate_.invalidate(); reason_ = reason;
}
bool safe(moveit::core::RobotState& state, const planning_scene::PlanningSceneConstPtr& scene) {
state.update();
return state.satisfiesBounds(group_) && !scene->isStateColliding(state, group_name_);
}
void command(const iiwa_msgs::msg::GizmoCommand& msg) {
auto state = current();
if (!state) { reject("joint state unavailable or stale"); return; }
if (!after_low_.empty() || !gate_.allowed(steady(), node_->now().seconds(), settled(*state))) return;
if (msg.epoch != gate_.epoch()) return;
if (!low_barrier_.idle()) return;
const auto& p = msg.target.pose;
const double age = (node_->now() - rclcpp::Time(msg.target.header.stamp)).seconds();
Eigen::Quaterniond q(p.orientation.w, p.orientation.x, p.orientation.y, p.orientation.z);
Eigen::Vector3d t(p.position.x, p.position.y, p.position.z);
if (msg.target.header.frame_id != frame_ || !q.coeffs().allFinite() || !t.allFinite() ||
std::abs(q.norm() - 1.) > .01 || age < -.01 || age > command_timeout_) {
reject("invalid or stale target"); return;
}
Eigen::Isometry3d requested = Eigen::Isometry3d::Identity();
requested.linear() = q.normalized().toRotationMatrix(); requested.translation() = t;
const auto actual = state->getGlobalLinkTransform(frame_).inverse() * state->getGlobalLinkTransform(tcp_);
if (!requiresGizmoMotion((requested.translation() - actual.translation()).norm(),
Eigen::Quaterniond(requested.linear()).angularDistance(Eigen::Quaterniond(actual.linear())))) {
gate_.invalidate();
reason_ = "target reached"; return;
}
const auto model_target = state->getGlobalLinkTransform(frame_) * requested;
// Solve once on release. setFromIK handles the tool's fixed TCP offset.
moveit::core::RobotState candidate(*state);
planning_scene_monitor::LockedPlanningSceneRO scene(scene_);
const bool valid = candidate.setFromIK(group_, model_target, tcp_, .01,
[this, &scene](moveit::core::RobotState* s, const moveit::core::JointModelGroup* group, const double* values) {
s->setJointGroupPositions(group, values); return safe(*s, scene);
});
if (!valid || !safe(candidate, scene)) { reject("target unreachable, outside limits or colliding"); return; }
// Do not accept approximate IK solutions as reachable targets.
const auto reached = candidate.getGlobalLinkTransform(tcp_);
if ((reached.translation() - model_target.translation()).norm() > .002 ||
Eigen::AngleAxisd(reached.linear().transpose() * model_target.linear()).angle() > .01) {
reject("IK solution does not reach target"); return;
}
candidate.copyJointGroupPositions(group_, ik_target_);
gate_.accept(msg.epoch, steady(), command_timeout_);
reason_ = "tracking";
}
void tick() {
flushHigh();
auto state = current();
if (!state) {
if (gate_.hasTarget()) reject("joint state unavailable or stale");
publishState({}, false, "joint state unavailable or stale"); return;
}
const bool available = after_low_.empty() && gate_.allowed(steady(), node_->now().seconds(), settled(*state));
if (!available) {
publishState(state, false, reason_); return;
}
// command_timeout validates the age at receipt. A released goal is a
// committed action and needs no stream of keepalive mouse commands.
if (gate_.hasTarget() && low_barrier_.idle()) {
try { step(state); }
catch (const std::exception& e) { reject(std::string("TCP trajectory error: ") + e.what()); }
}
publishState(state, true, reason_);
}
void step(const moveit::core::RobotStatePtr& state) {
const auto& names = group_->getVariableNames();
if (ik_target_.size() != names.size()) { reject("missing IK target"); return; }
std::vector<double> actual;
state->copyJointGroupPositions(group_, actual);
Eigen::VectorXd delta(names.size());
double duration = 2. * period_;
double max_travel = 0.;
for (std::size_t i = 0; i < names.size(); ++i) {
delta[i] = ik_target_[i] - actual[i];
const double travel = std::abs(delta[i]);
max_travel = std::max(max_travel, travel);
const auto& limit = state->getRobotModel()->getVariableBounds(names[i]);
// q(u) = q0 + (3u^2 - 2u^3) * delta, with zero endpoint velocities.
if (limit.velocity_bounded_)
duration = std::max(duration, 1.5 * travel / limit.max_velocity_);
if (limit.acceleration_bounded_)
duration = std::max(duration, std::sqrt(6. * travel / limit.max_acceleration_));
}
if (!delta.allFinite() || !std::isfinite(duration)) { reject("invalid trajectory limits"); return; }
// Validate the complete path, including the actual tool tip. Use forward
// Jacobians only to measure Cartesian speed; no inverse/singularity gate.
// Densely sample the cubic path and increase duration to obey BOTH TCP
// translation and rotation limits throughout it, not just at its endpoints.
const Eigen::Vector3d tcp_offset = (state->getGlobalLinkTransform(tip_).inverse() *
state->getGlobalLinkTransform(tcp_)).translation();
const auto* tip_link = state->getRobotModel()->getLinkModel(tip_);
const int samples = std::max(100, static_cast<int>(std::ceil(max_travel / .01)));
planning_scene_monitor::LockedPlanningSceneRO scene(scene_);
moveit::core::RobotState sample(*state);
Eigen::MatrixXd jacobian;
std::vector<double> positions(actual.size());
for (int k = 0; k <= samples; ++k) {
const double u = static_cast<double>(k) / samples;
const double blend = u * u * (3. - 2. * u);
for (std::size_t i = 0; i < positions.size(); ++i) positions[i] = actual[i] + blend * delta[i];
sample.setJointGroupPositions(group_, positions);
if (!safe(sample, scene)) { reject("TCP trajectory is outside limits or colliding"); return; }
if (!sample.getJacobian(group_, tip_link, tcp_offset, jacobian)) {
reject("cannot compute tool TCP speed"); return;
}
const Eigen::VectorXd twist = jacobian * delta * (6. * u * (1. - u));
duration = std::max(duration, 1.01 * twist.head<3>().norm() / linear_speed_);
duration = std::max(duration, 1.01 * twist.tail<3>().norm() / angular_speed_);
}
if (!std::isfinite(duration)) { reject("invalid TCP trajectory duration"); return; }
Trajectory message;
message.joint_names = names;
trajectory_msgs::msg::JointTrajectoryPoint start, end;
start.positions = actual;
start.velocities.assign(actual.size(), 0.);
start.time_from_start = rclcpp::Duration::from_seconds(.001);
end.positions = ik_target_;
end.velocities.assign(actual.size(), 0.);
end.time_from_start = rclcpp::Duration::from_seconds(duration + .001);
message.points = {start, end};
reason_ = "executing released TCP target";
sendLow(message);
}
void publishState(const moveit::core::RobotStatePtr& state, bool enabled, const std::string& reason) {
iiwa_msgs::msg::GizmoState message;
message.actual.header.frame_id = (state || have_actual_) ? frame_ : "";
message.actual.header.stamp = node_->now();
message.actual.pose = last_actual_;
if (state) {
const auto pose = state->getGlobalLinkTransform(frame_).inverse() * state->getGlobalLinkTransform(tcp_);
message.actual.pose.position.x = pose.translation().x(); message.actual.pose.position.y = pose.translation().y();
message.actual.pose.position.z = pose.translation().z();
const Eigen::Quaterniond q(pose.linear());
message.actual.pose.orientation.x = q.x(); message.actual.pose.orientation.y = q.y();
message.actual.pose.orientation.z = q.z(); message.actual.pose.orientation.w = q.w();
last_actual_ = message.actual.pose; have_actual_ = true;
}
message.enabled = enabled; message.epoch = gate_.epoch(); message.reason = reason;
state_pub_->publish(message);
}
rclcpp::Node::SharedPtr node_;
planning_scene_monitor::PlanningSceneMonitorPtr scene_;
std::shared_ptr<servo::ParamListener> listener_;
const moveit::core::JointModelGroup* group_{};
std::string group_name_, tcp_, tip_, frame_, reason_{"ready"};
double period_{}, command_timeout_{}, state_timeout_{}, linear_speed_{}, angular_speed_{};
GizmoPolicy gate_;
std::vector<double> ik_target_;
bool have_actual_{false};
LowGoalBarrier low_barrier_;
ClientGoal::SharedPtr low_goal_;
std::deque<std::function<void()>> after_low_;
std::map<std::string, std::uint64_t> lease_requests_;
std::map<rclcpp_action::GoalUUID, std::shared_ptr<ProxyGoal>> high_goals_;
std::size_t high_goals_count_{0};
rclcpp_action::Client<Follow>::SharedPtr action_client_;
rclcpp_action::Server<Follow>::SharedPtr action_server_;
std::vector<double> previous_;
double previous_time_{0.}, stable_since_{0.}, feedback_received_{0.};
std::int64_t feedback_stamp_{-1};
geometry_msgs::msg::Pose last_actual_{};
rclcpp::Publisher<Trajectory>::SharedPtr output_;
rclcpp::Publisher<iiwa_msgs::msg::GizmoState>::SharedPtr state_pub_;
rclcpp::Subscription<iiwa_msgs::msg::GizmoCommand>::SharedPtr command_sub_;
rclcpp::Subscription<Trajectory>::SharedPtr normal_sub_;
rclcpp::Service<Priority>::SharedPtr priority_;
rclcpp::TimerBase::SharedPtr timer_;
};
} // namespace iiwa_planning
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("tcp_gizmo", rclcpp::NodeOptions().automatically_declare_parameters_from_overrides(true));
try {
iiwa_planning::TcpGizmo gizmo(node);
// All motion, priority, target and timer callbacks are serialized.
rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node); executor.spin();
} catch (const std::exception& error) {
RCLCPP_FATAL(node->get_logger(), "%s", error.what()); rclcpp::shutdown(); return 1;
}
rclcpp::shutdown(); return 0;
}