diff --git a/nexus_capabilities/CMakeLists.txt b/nexus_capabilities/CMakeLists.txt
index 063bf86..2a5b15c 100644
--- a/nexus_capabilities/CMakeLists.txt
+++ b/nexus_capabilities/CMakeLists.txt
@@ -15,8 +15,12 @@ set(dep_pkgs
nexus_common
nexus_endpoints
nexus_orchestrator_msgs
+ nlohmann_json
rclcpp
rclcpp_lifecycle
+ rmf_dispenser_msgs
+ rmf_task_msgs
+ yaml_cpp_vendor
yaml-cpp
)
foreach(pkg ${dep_pkgs})
@@ -80,7 +84,7 @@ target_link_libraries(${PROJECT_NAME}
BT::behaviortree_cpp_v3
rclcpp::rclcpp
rclcpp_lifecycle::rclcpp_lifecycle
- yaml-cpp
+ yaml-cpp::yaml-cpp
)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
@@ -126,6 +130,8 @@ add_library(nexus_builtin_capabilities SHARED
src/capabilities/gripper_control.cpp
src/capabilities/plan_motion_capability.cpp
src/capabilities/plan_motion.cpp
+ src/capabilities/rmf_request.cpp
+ src/capabilities/rmf_request_capability.cpp
)
target_compile_options(nexus_builtin_capabilities PUBLIC INTERFACE cxx_std_17)
@@ -135,9 +141,13 @@ target_link_libraries(nexus_builtin_capabilities
${PROJECT_NAME}
nexus_common::nexus_common
nexus_endpoints::nexus_endpoints
+ nlohmann_json::nlohmann_json
pluginlib::pluginlib
tf2::tf2
tf2_ros::tf2_ros
+ ${rmf_dispenser_msgs_TARGETS}
+ ${rmf_task_msgs_TARGETS}
+ yaml-cpp
)
install(
diff --git a/nexus_capabilities/package.xml b/nexus_capabilities/package.xml
index 0ab7e9e..19c6a41 100644
--- a/nexus_capabilities/package.xml
+++ b/nexus_capabilities/package.xml
@@ -13,13 +13,17 @@
nexus_common
nexus_endpoints
nexus_orchestrator_msgs
+ nlohmann-json-dev
pluginlib
rclcpp
rclcpp_lifecycle
+ rmf_dispenser_msgs
+ rmf_task_msgs
rmf_utils
tf2
tf2_ros
yaml-cpp
+ yaml_cpp_vendor
ament_cmake
diff --git a/nexus_capabilities/src/capabilities/plugins.xml b/nexus_capabilities/src/capabilities/plugins.xml
index c2b87bf..85fe2c8 100644
--- a/nexus_capabilities/src/capabilities/plugins.xml
+++ b/nexus_capabilities/src/capabilities/plugins.xml
@@ -18,4 +18,8 @@
PlanMotion capability.
+
+
+ RMFRequest capability.
+
diff --git a/nexus_capabilities/src/capabilities/rmf_request.cpp b/nexus_capabilities/src/capabilities/rmf_request.cpp
new file mode 100644
index 0000000..b77c3c1
--- /dev/null
+++ b/nexus_capabilities/src/capabilities/rmf_request.cpp
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2025 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include "rmf_request.hpp"
+
+#include
+#include
+
+namespace nexus::capabilities {
+
+namespace rmf {
+
+BT::NodeStatus DispatchRequest::onStart()
+{
+ const auto destinations = this->getInput>("destinations");
+ if (!destinations)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [destinations] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+
+ const auto transient_qos =
+ rclcpp::SystemDefaultsQoS().transient_local().keep_last(10).reliable();
+ this->_api_request_pub = this->_node->create_publisher("/task_api_requests", transient_qos);
+ // TODO(luca) publish request here with unique UUID
+ this->_api_response_sub = this->_node->create_subscription("/task_api_responses", transient_qos,
+ [&](ApiResponse::UniquePtr msg)
+ {
+ this->api_response_cb(*msg);
+ });
+ this->submit_itinerary(*destinations);
+ return BT::NodeStatus::RUNNING;
+}
+
+void DispatchRequest::submit_itinerary(const std::deque& destinations)
+{
+ nlohmann::json j;
+ j["type"] = "dispatch_task_request";
+ nlohmann::json r;
+ r["unix_millis_request_time"] = 0;
+ r["unix_millis_earliest_start_time"] = 0;
+ r["requester"] = this->_node->get_name();
+ r["category"] = "compose";
+ nlohmann::json d;
+ d["category"] = "multi_delivery";
+ d["phases"] = nlohmann::json::array();
+ nlohmann::json activity;
+ activity["category"] = "sequence";
+ activity["description"]["activities"] = nlohmann::json::array();
+ for (const auto& destination : destinations)
+ {
+ nlohmann::json a;
+ a["category"] = destination.action;
+ nlohmann::json p;
+ // TODO(luca) exception safety for wrong types? Checking for pickup only since we don't do ingestors yet?
+ p["place"] = destination.workcell;
+ // TODO(luca) We should assign a handler that is related to the workcell.
+ // For now the assumption is that a location has only one handler
+ p["handler"] = destination.workcell;
+ p["payload"] = nlohmann::json::array();
+ a["description"] = p;
+ activity["description"]["activities"].push_back(a);
+ }
+ nlohmann::json act_obj;
+ act_obj["activity"] = activity;
+ d["phases"].push_back(act_obj);
+ r["description"] = d;
+ j["request"] = r;
+ ApiRequest msg;
+ msg.json_msg = j.dump();
+ // Time since epoch as a unique id
+ auto now = std::chrono::steady_clock::now().time_since_epoch();
+ msg.request_id = std::chrono::duration_cast(now).count();
+ this->requested_id = msg.request_id;
+ this->_api_request_pub->publish(msg);
+}
+
+void DispatchRequest::api_response_cb(const ApiResponse& msg)
+{
+ // Receive response, populate hashmaps
+ if (msg.type != msg.TYPE_RESPONDING)
+ {
+ return;
+ }
+ if (msg.request_id != this->requested_id)
+ {
+ return;
+ }
+ auto j = nlohmann::json::parse(msg.json_msg, nullptr, false);
+ if (j.is_discarded())
+ {
+ RCLCPP_ERROR(this->_node->get_logger(), "Invalid json in api response");
+ return;
+ }
+ // TODO(luca) exception safety for missing fields, return FAILURE if
+ // submission failed
+ if (j["success"] == false)
+ {
+ RCLCPP_ERROR(this->_node->get_logger(), "Task submission failed");
+ return;
+ }
+ // Task cancellations don't have a state field
+ if (!j.contains("state"))
+ return;
+ this->rmf_task_id = j["state"]["booking"]["id"];
+}
+
+BT::NodeStatus DispatchRequest::onRunning()
+{
+ if (rmf_task_id.has_value())
+ {
+ this->setOutput("rmf_task_id", *rmf_task_id);
+ return BT::NodeStatus::SUCCESS;
+ }
+ return BT::NodeStatus::RUNNING;
+}
+
+BT::NodeStatus ExtractDestinations::tick()
+{
+ const auto ctx = this->_ctx_mgr->current_context();
+ const auto& task = ctx->task;
+ std::deque destinations;
+ for (const auto& node : task.data)
+ {
+ if (node["type"] && node["destination"] && node["workcell_task_id"])
+ {
+ auto type = node["type"].as();
+ auto destination = node["destination"].as();
+ auto workcell_task_id = node["workcell_task_id"].as();
+ destinations.push_back(Destination {type, destination, workcell_task_id});
+ }
+ else
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(),
+ "Order element did not contain \"type\" and \"destination\" fields");
+ return BT::NodeStatus::FAILURE;
+ }
+ }
+ this->setOutput("destinations", destinations);
+ return BT::NodeStatus::SUCCESS;
+}
+
+BT::NodeStatus UnpackDestinationData::tick()
+{
+ const auto destination = this->getInput("destination");
+ if (!destination)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [destination] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+ this->setOutput("workcell", destination->workcell);
+ this->setOutput("type", destination->action);
+ this->setOutput("workcell_task_id", destination->workcell_task_id);
+ return BT::NodeStatus::SUCCESS;
+}
+
+BT::NodeStatus SignalAmr::tick()
+{
+ const auto workcell = this->getInput("workcell");
+ if (!workcell)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [workcell] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+ const auto rmf_task_id = this->getInput("rmf_task_id");
+ if (!rmf_task_id)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [rmf_task_id] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+ this->_dispenser_result_pub = this->_node->create_publisher("/dispenser_results", 10);
+
+ DispenserResult msg;
+ msg.request_guid = *rmf_task_id;
+ msg.source_guid = *workcell;
+ msg.status = DispenserResult::SUCCESS;
+ this->_dispenser_result_pub->publish(msg);
+ return BT::NodeStatus::SUCCESS;
+}
+
+BT::NodeStatus LoopDestinations::tick()
+{
+ if (!this->_initialized)
+ {
+ auto queue = this->getInput>("queue");
+ if (!queue)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [queue] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+ this->_queue = *queue;
+ this->_initialized = true;
+ }
+
+ if (this->_queue.size() == 0)
+ {
+ return BT::NodeStatus::SUCCESS;
+ }
+
+ this->setStatus(BT::NodeStatus::RUNNING);
+
+ while (this->_queue.size() > 0)
+ {
+ auto current_value = this->_queue.front();
+ this->setOutput("value", current_value);
+
+ this->setStatus(BT::NodeStatus::RUNNING);
+ auto child_state = this->child_node_->executeTick();
+ switch (child_state)
+ {
+ case BT::NodeStatus::SUCCESS:
+ this->haltChild();
+ this->_queue.pop_front();
+ break;
+ case BT::NodeStatus::RUNNING:
+ return BT::NodeStatus::RUNNING;
+ case BT::NodeStatus::FAILURE:
+ this->haltChild();
+ return BT::NodeStatus::FAILURE;
+ case BT::NodeStatus::IDLE:
+ throw BT::LogicError("A child node must never return IDLE");
+ }
+ }
+
+ return BT::NodeStatus::SUCCESS;
+}
+
+BT::NodeStatus WaitForAmr::onStart()
+{
+ const auto workcell = this->getInput("workcell");
+ if (!workcell)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [workcell] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+ const auto rmf_task_id = this->getInput("rmf_task_id");
+ if (!rmf_task_id)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: [rmf_task_id] port is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+
+ this->_workcell = *workcell;
+ this->_rmf_task_id = *rmf_task_id;
+ this->_amr_ready = false;
+
+ this->_dispenser_request_sub = this->_node->create_subscription("/dispenser_requests", 10,
+ [&](DispenserRequest::UniquePtr msg)
+ {
+ this->dispenser_request_cb(*msg);
+ });
+ return BT::NodeStatus::RUNNING;
+}
+
+void WaitForAmr::dispenser_request_cb(const DispenserRequest& msg)
+{
+ if (msg.request_guid == this->_rmf_task_id &&
+ msg.target_guid == this->_workcell)
+ {
+ this->_amr_ready = true;
+ }
+}
+
+BT::NodeStatus WaitForAmr::onRunning()
+{
+ if (this->_amr_ready)
+ {
+ this->_amr_ready = false;
+ return BT::NodeStatus::SUCCESS;
+ }
+ return BT::NodeStatus::RUNNING;
+}
+
+BT::NodeStatus SendSignal::tick()
+{
+ const auto ctx = this->_ctx_mgr->current_context();
+ auto signal = this->getInput("signal");
+ if (!signal)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: port [signal] is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+
+ auto workcell = this->getInput("workcell");
+ if (!workcell)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: port [workcell] is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+
+ auto workcell_task_id = this->getInput("workcell_task_id");
+ if (!workcell_task_id)
+ {
+ RCLCPP_ERROR(
+ this->_node->get_logger(), "%s: port [workcell_task_id] is required",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+
+ this->_signal_client = std::make_unique>(
+ this->_node, endpoints::SignalWorkcellService::service_name(*workcell));
+
+ auto req =
+ std::make_shared();
+ req->task_id = *workcell_task_id;
+ req->signal = *signal;
+ auto resp = this->_signal_client->send_request(req);
+ if (!resp->success)
+ {
+ // TODO(luca) implement a queueing mechanism if the request fails?
+ RCLCPP_WARN(
+ this->_node->get_logger(),
+ "%s: Workcell [%s] is not able to accept [%s] signal now. Skipping signaling, error: [%s]",
+ this->_node->get_name(), workcell->c_str(), signal->c_str(), resp->message.c_str());
+ }
+ return BT::NodeStatus::SUCCESS;
+}
+
+}
+}
diff --git a/nexus_capabilities/src/capabilities/rmf_request.hpp b/nexus_capabilities/src/capabilities/rmf_request.hpp
new file mode 100644
index 0000000..f0a8fb9
--- /dev/null
+++ b/nexus_capabilities/src/capabilities/rmf_request.hpp
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2025 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef NEXUS_WORKCELL_ORCHESTRATOR__RMF_REQUEST_HPP
+#define NEXUS_WORKCELL_ORCHESTRATOR__RMF_REQUEST_HPP
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+namespace nexus::capabilities {
+
+namespace rmf {
+
+struct Destination {
+ // Either "pickup" or "dropoff"
+ std::string action;
+ // Workcell to deliver to
+ std::string workcell;
+ // Task id that the workcell is executing for signaling AMR arrival
+ std::string workcell_task_id;
+};
+
+/**
+ * Returns RUNNING until a signal is received from system orchestrator.
+ *
+ * Input Ports:
+ * signal |std::string| Signal to wait for.
+ * clear |bool| Set this to true to clear the signal when this node is finished.
+ */
+class DispatchRequest : public BT::StatefulActionNode
+{
+// RMF interfaces
+public: using ApiRequest = rmf_task_msgs::msg::ApiRequest;
+public: using ApiResponse = rmf_task_msgs::msg::ApiResponse;
+
+public: static BT::PortsList providedPorts()
+ {
+ return { BT::InputPort>("destinations", "Destinations to visit"),
+ BT::OutputPort("rmf_task_id", "The resulting RMF task id")
+ };
+ }
+
+public: DispatchRequest(const std::string& name,
+ const BT::NodeConfiguration& config,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::StatefulActionNode(name, config), _node(std::move(node)) {}
+
+public: BT::NodeStatus onStart() override;
+
+public: BT::NodeStatus onRunning() override;
+
+public: void onHalted() override {}
+
+private: void submit_itinerary(const std::deque& destinations);
+
+private: void api_response_cb(const ApiResponse& msg);
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+
+private: rclcpp::Publisher::SharedPtr _api_request_pub;
+private: rclcpp::Subscription::SharedPtr _api_response_sub;
+
+// Populated by the API response callback
+private: std::optional rmf_task_id;
+private: std::string requested_id;
+};
+
+// Parses the current task in the context and returns a queue of destinations to visit
+class ExtractDestinations : public BT::SyncActionNode
+{
+public: static BT::PortsList providedPorts()
+ {
+ return {
+ BT::OutputPort>("destinations"),
+ };
+ }
+
+public: BT::NodeStatus tick() override;
+
+public: ExtractDestinations(const std::string& name,
+ const BT::NodeConfiguration& config,
+ std::shared_ptr ctx_mgr,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::SyncActionNode(name, config), _node(node), _ctx_mgr(ctx_mgr) {}
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+private: std::shared_ptr _ctx_mgr;
+};
+
+class UnpackDestinationData : public BT::SyncActionNode
+{
+public: static BT::PortsList providedPorts()
+ {
+ return {
+ BT::InputPort("destination"),
+ BT::OutputPort("workcell"),
+ BT::OutputPort("type"),
+ BT::OutputPort("workcell_task_id"),
+ };
+ }
+
+public: UnpackDestinationData(const std::string& name,
+ const BT::NodeConfiguration& config,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::SyncActionNode(name, config), _node(node) {}
+
+public: BT::NodeStatus tick() override;
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+};
+
+// TODO(luca) consider implementing ingestors as well, will need a new input for action type
+// Sends a DispenserResult to the AMR to signal that the workcell is done
+class SignalAmr : public BT::SyncActionNode
+{
+public: using DispenserResult = rmf_dispenser_msgs::msg::DispenserResult;
+public: static BT::PortsList providedPorts()
+ {
+ return {
+ BT::InputPort("workcell"),
+ BT::InputPort("rmf_task_id"),
+ };
+ }
+
+public: SignalAmr(const std::string& name,
+ const BT::NodeConfiguration& config,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::SyncActionNode(name, config), _node(node) {}
+
+public: BT::NodeStatus tick() override;
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+private: rclcpp::Publisher::SharedPtr _dispenser_result_pub;
+};
+
+// Loops over destinations to extract them
+// TODO(luca) use builtin LoopNode when migrating to behaviortreecpp v4
+class LoopDestinations : public BT::DecoratorNode
+{
+ /**
+ * Defines provided ports for the BT node.
+ */
+public: static BT::PortsList providedPorts()
+ {
+ return {
+ BT::InputPort>("queue"),
+ BT::OutputPort("value"),
+ };
+ }
+
+/**
+ * @param name name of the BT node.
+ * @param config config of the BT node.
+ */
+public: LoopDestinations(const std::string& name,
+ const BT::NodeConfiguration& config,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::DecoratorNode{name, config}, _node(node) {}
+
+public: BT::NodeStatus tick() override;
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+private: bool _initialized = false;
+private: std::deque _queue;
+};
+
+class WaitForAmr: public BT::StatefulActionNode
+{
+// RMF interfaces
+public: using DispenserRequest = rmf_dispenser_msgs::msg::DispenserRequest;
+
+public: static BT::PortsList providedPorts()
+ {
+ return { BT::InputPort("rmf_task_id"),
+ BT::InputPort("workcell")
+ };
+ }
+
+public: WaitForAmr(const std::string& name,
+ const BT::NodeConfiguration& config,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::StatefulActionNode(name, config), _node(std::move(node)) {}
+
+public: BT::NodeStatus onStart() override;
+
+public: BT::NodeStatus onRunning() override;
+
+public: void onHalted() override {}
+
+private: void dispenser_request_cb(const DispenserRequest& msg);
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+
+private: rclcpp::Subscription::SharedPtr _dispenser_request_sub;
+
+private: std::string _rmf_task_id;
+private: std::string _workcell;
+private: bool _amr_ready = false;
+};
+
+/**
+ * Sends a signal to the system orchestrator.
+ * The signal will be tied to the current task id
+ *
+ * Input Ports:
+ * signal |std::string| Signal to send.
+ */
+class SendSignal : public BT::SyncActionNode
+{
+public: using WorkcellTask = nexus_orchestrator_msgs::msg::WorkcellTask;
+public: static BT::PortsList providedPorts()
+ {
+ return {
+ BT::InputPort("signal", "Signal to send."),
+ BT::InputPort("workcell", "Workcell to signal."),
+ BT::InputPort("workcell_task_id", "Task id for the workcell to signal")
+ };
+ }
+
+public: SendSignal(const std::string& name, const BT::NodeConfiguration& config,
+ std::shared_ptr ctx_mgr,
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node)
+ : BT::SyncActionNode(name, config), _node(node), _ctx_mgr(ctx_mgr) {}
+
+public: BT::NodeStatus tick() override;
+
+private: std::unique_ptr>
+ _signal_client;
+
+private: rclcpp_lifecycle::LifecycleNode::SharedPtr _node;
+private: std::shared_ptr _ctx_mgr;
+};
+
+}
+}
+
+#endif
diff --git a/nexus_capabilities/src/capabilities/rmf_request_capability.cpp b/nexus_capabilities/src/capabilities/rmf_request_capability.cpp
new file mode 100644
index 0000000..f1c1842
--- /dev/null
+++ b/nexus_capabilities/src/capabilities/rmf_request_capability.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2025 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// #include
+
+#include "rmf_request.hpp"
+#include "rmf_request_capability.hpp"
+
+namespace nexus::capabilities {
+
+using namespace rmf;
+
+void RMFRequestCapability::configure(
+ rclcpp_lifecycle::LifecycleNode::SharedPtr node,
+ std::shared_ptr ctx_mgr,
+ BT::BehaviorTreeFactory& bt_factory)
+{
+ bt_factory.registerBuilder("rmf_request.DispatchRequest",
+ [this, node](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, node);
+ });
+
+ bt_factory.registerBuilder("rmf_request.ExtractDestinations",
+ [this, node, ctx_mgr](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, ctx_mgr, node);
+ });
+
+ bt_factory.registerBuilder("rmf_request.UnpackDestinationData",
+ [this, node](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, node);
+ });
+
+ bt_factory.registerBuilder("rmf_request.SignalAmr",
+ [this, node](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, node);
+ });
+
+ bt_factory.registerBuilder("rmf_request.LoopDestinations",
+ [this, node](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, node);
+ });
+
+ bt_factory.registerBuilder("rmf_request.WaitForAmr",
+ [this, node](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, node);
+ });
+
+ bt_factory.registerBuilder("rmf_request.SendSignal",
+ [this, node, ctx_mgr](const std::string& name,
+ const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config, ctx_mgr, node);
+ });
+}
+
+}
+
+#include
+
+PLUGINLIB_EXPORT_CLASS(
+ nexus::capabilities::RMFRequestCapability,
+ nexus::Capability
+)
diff --git a/nexus_capabilities/src/capabilities/rmf_request_capability.hpp b/nexus_capabilities/src/capabilities/rmf_request_capability.hpp
new file mode 100644
index 0000000..5505a52
--- /dev/null
+++ b/nexus_capabilities/src/capabilities/rmf_request_capability.hpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef NEXUS_CAPABILITIES__CAPABILITIES__RMF_REQUEST_CAPABILITY_HPP
+#define NEXUS_CAPABILITIES__CAPABILITIES__RMF_REQUEST_CAPABILITY_HPP
+
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include
+
+namespace nexus::capabilities {
+
+/**
+ * Capability to plan robot arm motions.
+ */
+class RMFRequestCapability : public Capability
+{
+ /**
+ * @copydoc Capability::declare_params
+ */
+public: void declare_params(rclcpp_lifecycle::LifecycleNode& /* node */) final
+ {
+ }
+
+ /**
+ * @copydoc Capability::configure
+ */
+public: void configure(rclcpp_lifecycle::LifecycleNode::SharedPtr node,
+ std::shared_ptr ctx_mgr,
+ BT::BehaviorTreeFactory& bt_factory) final;
+
+ /**
+ * @copydoc Capability::activate
+ */
+public: void activate() final {}
+
+ /**
+ * @copydoc Capability::deactivate
+ */
+public: void deactivate() final {}
+};
+
+}
+
+#endif
diff --git a/nexus_integration_tests/CMakeLists.txt b/nexus_integration_tests/CMakeLists.txt
index ea52012..e8c1518 100644
--- a/nexus_integration_tests/CMakeLists.txt
+++ b/nexus_integration_tests/CMakeLists.txt
@@ -176,4 +176,63 @@ if(BUILD_TESTING)
endif()
endif()
+# Modified from rmf_demos_maps
+# Get absolute path of the building map
+set(building_map "config/rmf/maps/depot/depot.building.yaml")
+get_filename_component(building_map_path ${building_map} REALPATH)
+
+# Get the output world name
+string(REGEX REPLACE "\\.[^.]*\.[^.]*$" "" no_extension_path ${building_map_path})
+string(REGEX MATCH "[^\/]+$" world_name ${no_extension_path})
+
+set(output_world_name ${world_name})
+set(output_dir ${CMAKE_CURRENT_BINARY_DIR}/config/rmf/maps/${output_world_name})
+set(output_world_path ${output_dir}/${output_world_name}.world)
+set(output_model_dir ${output_dir}/models)
+
+##############################################################################
+# Generate Gz world, download Models, change robot to use Fuel model
+##############################################################################
+
+message("BUILDING WORLDFILE WITH COMMAND: ros2 run rmf_building_map_tools building_map_generator gazebo ${building_map_path} ${output_world_path} ${output_model_dir}")
+message("REPLACING ROBOT MODELS WITH THOSE FROM FUEL")
+if (NO_DOWNLOAD_MODELS)
+ add_custom_command(
+ DEPENDS ${building_map_path}
+ COMMAND ros2 run rmf_building_map_tools building_map_generator gazebo ${building_map_path} ${output_world_path} ${output_model_dir} --TEMPLATE_WORLD_FILE ${CMAKE_CURRENT_SOURCE_DIR}/config/rmf/maps/depot/depot_template.sdf --SKIP_CAMERA_POSE
+ COMMAND sed -i 's|model:\/\/DeliveryRobot<\/uri>|https:\/\/fuel.gazebosim.org\/1.0\/Open-RMF\/models\/DeliveryRobotWithConveyor<\/uri>|g' ${output_world_path}
+ OUTPUT ${output_world_path}
+ )
+else()
+ message("DOWNLOADING MODELS WITH COMMAND: ros2 run rmf_building_map_tools building_map_model_downloader ${building_map_path}")
+ add_custom_command(
+ DEPENDS ${building_map_path}
+ COMMAND ros2 run rmf_building_map_tools building_map_generator gazebo ${building_map_path} ${output_world_path} ${output_model_dir} --TEMPLATE_WORLD_FILE ${CMAKE_CURRENT_SOURCE_DIR}/config/rmf/maps/depot/depot_template.sdf --SKIP_CAMERA_POSE
+ COMMAND sed -i 's|model:\/\/DeliveryRobot<\/uri>|https:\/\/fuel.gazebosim.org\/1.0\/Open-RMF\/models\/DeliveryRobotWithConveyor<\/uri>|g' ${output_world_path}
+ COMMAND ros2 run rmf_building_map_tools building_map_model_downloader ${building_map_path} -e ~/.gazebo/models
+ OUTPUT ${output_world_path}
+ )
+endif()
+
+##############################################################################
+# Generate the nav graphs
+##############################################################################
+
+set(output_nav_graphs_dir ${output_dir}/nav_graphs/)
+set(output_nav_graphs_phony ${output_nav_graphs_dir}/phony)
+add_custom_command(
+ OUTPUT ${output_nav_graphs_phony}
+ COMMAND ros2 run rmf_building_map_tools building_map_generator nav ${building_map_path} ${output_nav_graphs_dir}
+ DEPENDS ${building_map_path} ${output_world_path}
+)
+
+add_custom_target(generate_${output_world_name}_nav_graphs ALL
+ DEPENDS ${output_nav_graphs_phony}
+)
+
+install(
+ DIRECTORY ${output_dir}
+ DESTINATION share/${PROJECT_NAME}/config/rmf/maps
+)
+
ament_package()
diff --git a/nexus_integration_tests/config/rmf/deliveryRobot_config.yaml b/nexus_integration_tests/config/rmf/deliveryRobot_config.yaml
new file mode 100644
index 0000000..2fcea94
--- /dev/null
+++ b/nexus_integration_tests/config/rmf/deliveryRobot_config.yaml
@@ -0,0 +1,46 @@
+# FLEET CONFIG =================================================================
+# RMF Fleet parameters
+
+rmf_fleet:
+ name: "deliveryRobot"
+ limits:
+ linear: [0.7, 0.75] # velocity, acceleration
+ angular: [0.6, 2.0] # velocity, acceleration
+ profile: # Robot profile is modelled as a circle
+ footprint: 0.6 # radius in m
+ vicinity: 0.8 # radius in m
+ reversible: True # whether robots in this fleet can reverse
+ # TODO Update battery parameters with actual specs
+ battery_system:
+ voltage: 24.0 # V
+ capacity: 40.0 # Ahr
+ charging_current: 8.8 # A
+ mechanical_system:
+ mass: 70.0 # kg
+ moment_of_inertia: 40.0 #kgm^2
+ friction_coefficient: 0.22
+ ambient_system:
+ power: 20.0 # W
+ tool_system:
+ power: 0.0 # W
+ recharge_threshold: 0.10 # Battery level below which robots in this fleet will not operate
+ recharge_soc: 1.0 # Battery level to which robots in this fleet should be charged up to during recharging tasks
+ max_delay: 15.0 # allowed seconds of delay of the current itinerary before it gets interrupted and replanned
+ publish_fleet_state: 10.0 # Publish frequency for fleet state, ensure that it is same as robot_state_update_frequency
+ account_for_battery_drain: True
+ task_capabilities: # Specify the types of RMF Tasks that robots in this fleet are capable of performing
+ loop: True
+ delivery: True
+ finishing_request: "charge" # [park, charge, nothing]
+ robots:
+ deliveryRobot1:
+ charger: "deliveryRobotCharger1"
+ deliveryRobot2:
+ charger: "deliveryRobotCharger2"
+
+fleet_manager:
+ ip: "127.0.0.1"
+ port: 22012
+ user: "some_user"
+ password: "some_password"
+ robot_state_update_frequency: 10.0
diff --git a/nexus_integration_tests/config/rmf/maps/depot/depot.building.yaml b/nexus_integration_tests/config/rmf/maps/depot/depot.building.yaml
new file mode 100644
index 0000000..5da70b1
--- /dev/null
+++ b/nexus_integration_tests/config/rmf/maps/depot/depot.building.yaml
@@ -0,0 +1,91 @@
+coordinate_system: reference_image
+crowd_sim:
+ agent_groups:
+ - {agents_name: [deliveryRobot1, deliveryRobot2], agents_number: 2, group_id: 0, profile_selector: external_agent, state_selector: external_static, x: 0, y: 0}
+ agent_profiles:
+ - {ORCA_tau: 1, ORCA_tauObst: 0.40000000000000002, class: 1, max_accel: 0, max_angle_vel: 0, max_neighbors: 10, max_speed: 0, name: external_agent, neighbor_dist: 5, obstacle_set: 1, pref_speed: 0, r: 0.25}
+ enable: 0
+ goal_sets: []
+ model_types: []
+ obstacle_set: {class: 1, file_name: L1_navmesh.nav, type: nav_mesh}
+ states:
+ - {final: 1, goal_set: -1, name: external_static, navmesh_file_name: ""}
+ transitions: []
+ update_time_step: 0.10000000000000001
+graphs:
+ {}
+levels:
+ L1:
+ drawing:
+ filename: depot_scan.png
+ elevation: 0
+ lanes:
+ - [14, 28, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [28, 15, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [30, 16, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [31, 17, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [32, 19, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [28, 34, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [34, 33, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [34, 29, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [29, 30, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [30, 31, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [31, 32, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [32, 35, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [35, 20, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [35, 36, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [36, 21, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [36, 37, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [37, 18, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [37, 38, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [38, 22, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [38, 29, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [27, 32, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [31, 36, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ - [30, 37, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 0], mutex: [1, ""], orientation: [1, ""], speed_limit: [3, 0]}]
+ layers:
+ {}
+ measurements:
+ - [0, 1, {distance: [3, 30.199999999999999]}]
+ vertices:
+ - [0.104, 307.17000000000002, 0, ""]
+ - [604.029, 307.00799999999998, 0, ""]
+ - [2.0099999999999998, 1.994, 0, ""]
+ - [602.91899999999998, 1.0029999999999999, 0, ""]
+ - [603.96900000000005, 301.74000000000001, 0, ""]
+ - [1.7090000000000001, 304.33499999999998, 0, ""]
+ - [147.125, 72.120000000000005, 0, ""]
+ - [157.90799999999999, 72.030000000000001, 0, ""]
+ - [158.08799999999999, 82.813999999999993, 0, ""]
+ - [146.76499999999999, 82.903999999999996, 0, ""]
+ - [147.178, 222.09899999999999, 0, ""]
+ - [158.00399999999999, 222.04900000000001, 0, ""]
+ - [158.05500000000001, 233.12899999999999, 0, ""]
+ - [147.02600000000001, 233.02699999999999, 0, ""]
+ - [211.29400000000001, 284.73700000000002, 0, deliveryRobotCharger1, {is_charger: [4, true], is_holding_point: [4, true], is_parking_spot: [4, true], spawn_robot_name: [1, deliveryRobot1], spawn_robot_type: [1, DeliveryRobot]}]
+ - [240.38200000000001, 284.73700000000002, 0, deliveryRobotCharger2, {is_charger: [4, true], is_holding_point: [4, true], is_parking_spot: [4, true], spawn_robot_name: [1, deliveryRobot2], spawn_robot_type: [1, DeliveryRobot]}]
+ - [160.90700000000001, 179.114, 0, workcell_1, {dropoff_ingestor: [1, workcell_1], is_holding_point: [4, false], is_parking_spot: [4, false], pickup_dispenser: [1, workcell_1]}]
+ - [100.842, 179.41800000000001, 0, workcell_2, {dropoff_ingestor: [1, workcell_2], is_holding_point: [4, false], is_parking_spot: [4, false], pickup_dispenser: [1, workcell_2]}]
+ - [160.91300000000001, 128.69999999999999, 0, workcell_4, {dropoff_ingestor: [1, workcell_4], pickup_dispenser: [1, workcell_4]}]
+ - [40.776000000000003, 180.02600000000001, 0, workcell_3, {dropoff_ingestor: [1, workcell_3], pickup_dispenser: [1, workcell_3]}]
+ - [41.079999999999998, 128.93199999999999, 0, workcell_6, {dropoff_ingestor: [1, workcell_6], pickup_dispenser: [1, workcell_6]}]
+ - [101.754, 128.93199999999999, 0, workcell_5, {dropoff_ingestor: [1, workcell_5], pickup_dispenser: [1, workcell_5]}]
+ - [228.02099999999999, 128.25200000000001, 0, "", {is_holding_point: [4, true], is_parking_spot: [4, true]}]
+ - [28.983000000000001, 108.464, 0, "", {is_holding_point: [4, true], is_parking_spot: [4, true]}]
+ - [25.177, 123.68899999999999, 0, "", {is_holding_point: [4, true], is_parking_spot: [4, true]}]
+ - [20.492000000000001, 81.236000000000004, 0, "", {is_holding_point: [4, true], is_parking_spot: [4, true]}]
+ - [43.536999999999999, 88.259, 0, "", {is_holding_point: [4, true], is_parking_spot: [4, true]}]
+ - [18.771999999999998, 164.773, 0, "", {is_holding_point: [4, true], is_parking_spot: [4, true]}]
+ - [225.733, 250.20699999999999, 0, ""]
+ - [227.39699999999999, 165.74100000000001, 0, ""]
+ - [161.04400000000001, 165.33600000000001, 0, ""]
+ - [101.2, 165.006, 0, ""]
+ - [40.965000000000003, 164.52699999999999, 0, ""]
+ - [191.227, 206.31100000000001, 0, ""]
+ - [226.608, 206.29900000000001, 0, ""]
+ - [41.436, 144.524, 0, ""]
+ - [101.277, 144.983, 0, ""]
+ - [160.523, 145.184, 0, ""]
+ - [227.73699999999999, 145.48099999999999, 0, ""]
+lifts: {}
+name: depot
diff --git a/nexus_integration_tests/config/rmf/maps/depot/depot_scan.png b/nexus_integration_tests/config/rmf/maps/depot/depot_scan.png
new file mode 100644
index 0000000..bfc74d9
Binary files /dev/null and b/nexus_integration_tests/config/rmf/maps/depot/depot_scan.png differ
diff --git a/nexus_integration_tests/config/rmf/maps/depot/depot_template.sdf b/nexus_integration_tests/config/rmf/maps/depot/depot_template.sdf
new file mode 100644
index 0000000..f1fa60b
--- /dev/null
+++ b/nexus_integration_tests/config/rmf/maps/depot/depot_template.sdf
@@ -0,0 +1,463 @@
+
+
+
+
+ 0.01
+ 1.0
+
+
+
+
+
+
+
+
+
+ 1 1 1
+ 0.8 0.8 0.8
+ false
+
+
+
+
+
+ 1000
+ 845
+
+
+
+
+
+
+
+
+
+
+ 3D View
+ false
+ docked
+
+ ogre2
+ scene
+ 0.4 0.4 0.4
+ 0.8 0.8 0.8
+ 2.232 -2.793 5.163 0 0.4496533 -1.0853471
+
+
+
+
+
+ floating
+ 5
+ 5
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+ false
+ 5
+ 5
+ floating
+ false
+
+
+
+
+
+
+ World control
+ false
+ false
+ 72
+ 1
+ floating
+
+
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+
+ World stats
+ false
+ false
+ 110
+ 290
+ 1
+ floating
+
+
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+
+ false
+ 0
+ 0
+ 250
+ 50
+ floating
+ false
+ #666666
+
+
+
+
+
+
+ false
+ 250
+ 0
+ 150
+ 50
+ floating
+ false
+ #666666
+
+
+
+
+
+
+ false
+ 0
+ 50
+ 250
+ 50
+ floating
+ false
+ #777777
+
+
+
+
+
+
+ false
+ 250
+ 50
+ 50
+ 50
+ floating
+ false
+ #777777
+
+
+
+
+
+
+ false
+ 300
+ 50
+ 100
+ 50
+ floating
+ false
+ #777777
+
+
+
+
+
+
+ false
+ docked
+
+
+
+
+
+
+ false
+ docked
+
+
+
+
+
+
+
+ true
+ 0 0 10 0 0 0
+ 1 1 1 1
+ 0.2 0.2 0.2 1
+
+ 1000
+ 0.09
+ 0.001
+ 0.001
+
+ -0.5 0.1 -0.9
+
+
+
+ https://fuel.gazebosim.org/1.0/OpenRobotics/models/Depot
+ 15.15 -7.525 0 0 0 0
+
+
+
+ 8 -10.3 0 0 0 3.14159
+ true
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/enclosure
+ enclosure
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_front
+ 0 -0.55 0 0 0 0
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_right
+ 0.55 0 0 0 0 1.5708
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_left
+ -0.55 0 0 0 0 1.5708
+ true
+
+
+
+
+ 5 -10.3 0 0 0 3.14159
+ true
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/enclosure
+ enclosure
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_front
+ 0 -0.55 0 0 0 0
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_right
+ 0.55 0 0 0 0 1.5708
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_left
+ -0.55 0 0 0 0 1.5708
+ true
+
+
+
+
+ 2 -10.3 0 0 0 3.14159
+ true
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/enclosure
+ enclosure
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_front
+ 0 -0.55 0 0 0 0
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_right
+ 0.55 0 0 0 0 1.5708
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_left
+ -0.55 0 0 0 0 1.5708
+ true
+
+
+
+
+ 8 -5 0 0 0 0
+ true
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/enclosure
+ enclosure
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_front
+ 0 -0.55 0 0 0 0
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_right
+ 0.55 0 0 0 0 1.5708
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_left
+ -0.55 0 0 0 0 1.5708
+ true
+
+
+
+
+ 5 -5 0 0 0 0
+ true
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/enclosure
+ enclosure
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_front
+ 0 -0.55 0 0 0 0
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_right
+ 0.55 0 0 0 0 1.5708
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_left
+ -0.55 0 0 0 0 1.5708
+ true
+
+
+
+
+ 2 -5 0 0 0 0
+ true
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/enclosure
+ enclosure
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_front
+ 0 -0.55 0 0 0 0
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_right
+ 0.55 0 0 0 0 1.5708
+ true
+
+
+ https://fuel.gazebosim.org/1.0/Open-RMF/models/conveyor_block
+ conveyor_block_left
+ -0.55 0 0 0 0 1.5708
+ true
+
+
+
+
+
diff --git a/nexus_integration_tests/config/rmf/transportation.xml b/nexus_integration_tests/config/rmf/transportation.xml
new file mode 100644
index 0000000..70148de
--- /dev/null
+++ b/nexus_integration_tests/config/rmf/transportation.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nexus_integration_tests/config/system_bts/main_rmf.xml b/nexus_integration_tests/config/system_bts/main_rmf.xml
new file mode 100644
index 0000000..9bbfb5d
--- /dev/null
+++ b/nexus_integration_tests/config/system_bts/main_rmf.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nexus_integration_tests/config/system_bts/pick_and_place_rmf.xml b/nexus_integration_tests/config/system_bts/pick_and_place_rmf.xml
new file mode 100644
index 0000000..4351d33
--- /dev/null
+++ b/nexus_integration_tests/config/system_bts/pick_and_place_rmf.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nexus_integration_tests/launch/control_center.launch.py b/nexus_integration_tests/launch/inter_workcell.launch.py
similarity index 63%
rename from nexus_integration_tests/launch/control_center.launch.py
rename to nexus_integration_tests/launch/inter_workcell.launch.py
index e9f62d2..a1c42c3 100644
--- a/nexus_integration_tests/launch/control_center.launch.py
+++ b/nexus_integration_tests/launch/inter_workcell.launch.py
@@ -17,6 +17,7 @@
import launch_ros
from launch_ros.actions import Node, LifecycleNode
+from launch_ros.descriptions import ParameterValue
from launch_ros.event_handlers import OnStateTransition
from launch_ros.substitutions import FindPackageShare
import lifecycle_msgs
@@ -106,6 +107,8 @@ def activate_node(target_node: LifecycleNode, depend_node: LifecycleNode = None)
def launch_setup(context, *args, **kwargs):
ros_domain_id = LaunchConfiguration("ros_domain_id")
+ use_fake_hardware = LaunchConfiguration("use_fake_hardware")
+ use_rmf_transporter = LaunchConfiguration("use_rmf_transporter")
use_zenoh_bridge = LaunchConfiguration("use_zenoh_bridge")
zenoh_config_package = LaunchConfiguration("zenoh_config_package")
zenoh_config_filename = LaunchConfiguration("zenoh_config_filename")
@@ -113,10 +116,10 @@ def launch_setup(context, *args, **kwargs):
activate_system_orchestrator = LaunchConfiguration("activate_system_orchestrator")
headless = LaunchConfiguration("headless")
main_bt_filename = LaunchConfiguration("main_bt_filename")
-
- nexus_panel_rviz_path = os.path.join(
- get_package_share_directory("nexus_integration_tests"), "rviz", "nexus_panel.rviz"
- )
+ remap_task_types = LaunchConfiguration("remap_task_types")
+ nexus_rviz_config = LaunchConfiguration("nexus_rviz_config")
+ system_orchestrator_bt_dir = LaunchConfiguration("system_orchestrator_bt_dir")
+ max_jobs = LaunchConfiguration("max_jobs")
system_orchestrator_node = LifecycleNode(
name="system_orchestrator",
@@ -125,20 +128,35 @@ def launch_setup(context, *args, **kwargs):
executable="nexus_system_orchestrator",
parameters=[
{
- "bt_path": (
- FindPackageShare("nexus_integration_tests"),
- "/config/system_bts",
- ),
- "remap_task_types":
- """{
- pick_and_place: [place_on_conveyor, pick_from_conveyor],
- }""",
+ "bt_path": system_orchestrator_bt_dir,
+ "remap_task_types": ParameterValue(remap_task_types, value_type=str),
"main_bt_filename": main_bt_filename,
- "max_jobs": 2,
+ "max_jobs": max_jobs,
}
],
)
+ rmf_transporter = GroupAction(
+ actions=[
+ IncludeLaunchDescription(
+ [
+ PathJoinSubstitution(
+ [
+ FindPackageShare("nexus_integration_tests"),
+ "launch",
+ "rmf_transporter.launch.xml",
+ ]
+ )
+ ],
+ launch_arguments={
+ "use_simulator": use_fake_hardware,
+ "headless": headless,
+ }.items(),
+ condition=IfCondition(use_rmf_transporter),
+ )
+ ],
+ )
+
transporter_node = LifecycleNode(
namespace="",
package="nexus_transporter",
@@ -146,11 +164,19 @@ def launch_setup(context, *args, **kwargs):
name="transporter_node",
parameters=[
{"transporter_plugin": transporter_plugin},
- {"destinations": ["loading", "unloading", "workcell_1", "workcell_2"]},
+ {"destinations": ["loading", "workcell_1", "workcell_2", "unloading",]},
{"x_increment": 1.0},
{"speed": 1.0},
{"unloading_station": "unloading"},
],
+ condition=UnlessCondition(use_rmf_transporter),
+ )
+
+ activate_transporter_node = GroupAction(
+ [
+ activate_node(transporter_node),
+ ],
+ condition=UnlessCondition(use_rmf_transporter),
)
mock_emergency_alarm_node = LifecycleNode(
@@ -164,44 +190,49 @@ def launch_setup(context, *args, **kwargs):
package="rviz2",
executable="rviz2",
name="nexus_panel",
- arguments=["-d", nexus_panel_rviz_path],
+ arguments=["-d", nexus_rviz_config.perform(context)],
condition=UnlessCondition(headless),
)
+ zenoh_bridge = GroupAction(
+ [
+ IncludeLaunchDescription(
+ [
+ PathJoinSubstitution(
+ [
+ FindPackageShare("nexus_integration_tests"),
+ "launch",
+ "zenoh_bridge.launch.py",
+ ]
+ )
+ ],
+ launch_arguments={
+ "zenoh_config_package": zenoh_config_package,
+ "zenoh_config_filename": zenoh_config_filename,
+ "ros_domain_id": ros_domain_id.perform(context),
+ }.items(),
+ )
+ ],
+ condition=IfCondition(use_zenoh_bridge),
+ )
+
+ activate_system_orchestrator = GroupAction(
+ [
+ activate_node(system_orchestrator_node),
+ ],
+ condition=IfCondition(activate_system_orchestrator),
+ )
+
return [
SetEnvironmentVariable("ROS_DOMAIN_ID", ros_domain_id),
system_orchestrator_node,
+ rmf_transporter,
transporter_node,
mock_emergency_alarm_node,
nexus_panel,
- GroupAction(
- [
- IncludeLaunchDescription(
- [
- PathJoinSubstitution(
- [
- FindPackageShare("nexus_integration_tests"),
- "launch",
- "zenoh_bridge.launch.py",
- ]
- )
- ],
- launch_arguments={
- "zenoh_config_package": zenoh_config_package,
- "zenoh_config_filename": zenoh_config_filename,
- "ros_domain_id": ros_domain_id.perform(context),
- }.items(),
- )
- ],
- condition=IfCondition(use_zenoh_bridge),
- ),
- GroupAction(
- [
- activate_node(system_orchestrator_node),
- ],
- condition=IfCondition(activate_system_orchestrator),
- ),
- activate_node(transporter_node),
+ zenoh_bridge,
+ activate_system_orchestrator,
+ activate_transporter_node,
activate_node(mock_emergency_alarm_node),
]
@@ -215,6 +246,17 @@ def generate_launch_description():
default_value="0",
description="ROS_DOMAIN_ID environment variable",
),
+ DeclareLaunchArgument(
+ "use_fake_hardware",
+ default_value="true",
+ description="Set True if running with real hardware.",
+ ),
+ DeclareLaunchArgument(
+ "use_rmf_transporter",
+ default_value="false",
+ description="Set true to rely on an Open-RMF managed fleet to transport material\
+ between workcells.",
+ ),
DeclareLaunchArgument(
"use_zenoh_bridge",
default_value="true",
@@ -250,6 +292,26 @@ def generate_launch_description():
default_value="main.xml",
description="File name of the main system orchestrator behavior tree",
),
+ DeclareLaunchArgument(
+ "remap_task_types",
+ default_value="\"pick_and_place: [place_on_conveyor, pick_from_conveyor]\"",
+ description="File name of the main system orchestrator behavior tree",
+ ),
+ DeclareLaunchArgument(
+ "nexus_rviz_config",
+ default_value=os.path.join(get_package_share_directory("nexus_integration_tests"), "rviz", "nexus_panel.rviz"),
+ description="Absolute path to an RViZ config file.",
+ ),
+ DeclareLaunchArgument(
+ "system_orchestrator_bt_dir",
+ default_value=os.path.join(get_package_share_directory("nexus_integration_tests"), "config", "system_bts"),
+ description="Absolute path directory containing BTs for the system orchestrator.",
+ ),
+ DeclareLaunchArgument(
+ "max_jobs",
+ default_value="2",
+ description="Maximum number of jobs the system orchestrator can process in parallel.",
+ ),
OpaqueFunction(function=launch_setup),
]
)
diff --git a/nexus_integration_tests/launch/launch.py b/nexus_integration_tests/launch/launch.py
index ff4567b..2f539d7 100644
--- a/nexus_integration_tests/launch/launch.py
+++ b/nexus_integration_tests/launch/launch.py
@@ -15,6 +15,7 @@
import os
import sys
+from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
@@ -41,6 +42,7 @@ def launch_setup(context, *args, **kwargs):
exit(1)
headless = LaunchConfiguration("headless")
+ use_rmf_transporter = LaunchConfiguration("use_rmf_transporter")
use_zenoh_bridge = LaunchConfiguration("use_zenoh_bridge")
use_fake_hardware = LaunchConfiguration("use_fake_hardware")
robot1_ip = LaunchConfiguration("robot1_ip")
@@ -48,23 +50,23 @@ def launch_setup(context, *args, **kwargs):
run_workcell_1 = LaunchConfiguration("run_workcell_1")
run_workcell_2 = LaunchConfiguration("run_workcell_2")
- control_center_domain_id = 0
+ inter_workcell_domain_id = 0
workcell_1_domain_id = 0
workcell_2_domain_id = 0
log_msg = ""
if "ROS_DOMAIN_ID" in os.environ:
- control_center_domain_id = int(os.environ["ROS_DOMAIN_ID"])
- if not 0 < control_center_domain_id < 230:
+ inter_workcell_domain_id = int(os.environ["ROS_DOMAIN_ID"])
+ if not 0 < inter_workcell_domain_id < 230:
log_msg += (
"ROS_DOMAIN_ID not within the range of 0 to 230, setting it to 0. \n"
)
- control_center_domain_id = 0
+ inter_workcell_domain_id = 0
if use_zenoh_bridge.perform(context).lower() == "true":
log_msg += "Using the zenoh bridge\n"
- workcell_1_domain_id = control_center_domain_id + 1
- workcell_2_domain_id = control_center_domain_id + 2
+ workcell_1_domain_id = inter_workcell_domain_id + 1
+ workcell_2_domain_id = inter_workcell_domain_id + 2
else:
log_msg += "Not using zenoh bridge\n"
if (
@@ -73,15 +75,31 @@ def launch_setup(context, *args, **kwargs):
):
print("To run both workcells, enable the Zenoh Bridge")
sys.exit(1)
- workcell_1_domain_id = control_center_domain_id
- workcell_2_domain_id = control_center_domain_id
- log_msg += f"Control Center has ROS_DOMAIN_ID {control_center_domain_id}\n"
+ workcell_1_domain_id = inter_workcell_domain_id
+ workcell_2_domain_id = inter_workcell_domain_id
+ log_msg += f"Inter-workcell has ROS_DOMAIN_ID {inter_workcell_domain_id}\n"
if run_workcell_1.perform(context).lower() == "true":
log_msg += f"Workcell 1 has ROS_DOMAIN_ID {workcell_1_domain_id}\n"
if run_workcell_2.perform(context).lower() == "true":
log_msg += f"Workcell 2 has ROS_DOMAIN_ID {workcell_2_domain_id}\n"
- launch_control_center = GroupAction(
+ main_bt_filename = "main.xml"
+ remap_task_types = """{
+ pick_and_place: [place_on_conveyor, pick_from_conveyor],
+ }"""
+ rviz_config_filename = "nexus_panel.rviz"
+ if (use_rmf_transporter.perform(context).lower() == "true"):
+ remap_task_types = """{
+ pick_and_place_rmf: [place_on_conveyor, pick_from_conveyor],
+ }"""
+ main_bt_filename = "main_rmf.xml"
+ rviz_config_filename = "nexus_panel_rmf.rviz"
+
+ log_msg += f"System Orchestrator will load : {main_bt_filename}\n"
+ nexus_rviz_config = os.path.join(
+ get_package_share_directory("nexus_integration_tests"), "rviz", rviz_config_filename)
+
+ launch_inter_workcell = GroupAction(
actions=[
IncludeLaunchDescription(
[
@@ -89,17 +107,21 @@ def launch_setup(context, *args, **kwargs):
[
FindPackageShare("nexus_integration_tests"),
"launch",
- "control_center.launch.py",
+ "inter_workcell.launch.py",
]
)
],
launch_arguments={
- "ros_domain_id": str(control_center_domain_id),
+ "ros_domain_id": str(inter_workcell_domain_id),
"zenoh_config_package": "nexus_integration_tests",
"zenoh_config_filename": "config/zenoh/system_orchestrator.json5",
+ "use_rmf_transporter": use_rmf_transporter,
"transporter_plugin": "nexus_transporter::MockTransporter",
"activate_system_orchestrator": headless,
"headless": headless,
+ "main_bt_filename": main_bt_filename,
+ "remap_task_types": remap_task_types,
+ "nexus_rviz_config": nexus_rviz_config,
}.items(),
),
],
@@ -193,7 +215,7 @@ def launch_setup(context, *args, **kwargs):
return [
LogInfo(msg=log_msg),
- launch_control_center,
+ launch_inter_workcell,
launch_workcell_1,
launch_workcell_2,
]
@@ -207,6 +229,12 @@ def generate_launch_description():
default_value="true",
description="Launch in headless mode (no gui)",
),
+ DeclareLaunchArgument(
+ "use_rmf_transporter",
+ default_value="false",
+ description="Set true to rely on an Open-RMF managed fleet to transport material\
+ between workcells.",
+ ),
DeclareLaunchArgument(
"use_zenoh_bridge",
default_value="true",
diff --git a/nexus_integration_tests/launch/rmf_transporter.launch.xml b/nexus_integration_tests/launch/rmf_transporter.launch.xml
new file mode 100644
index 0000000..96ac0a9
--- /dev/null
+++ b/nexus_integration_tests/launch/rmf_transporter.launch.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nexus_integration_tests/package.xml b/nexus_integration_tests/package.xml
index 2cb79d9..74fed61 100644
--- a/nexus_integration_tests/package.xml
+++ b/nexus_integration_tests/package.xml
@@ -21,6 +21,7 @@
rclcpp_components
rclcpp_lifecycle
rclpy
+ rmf_fleet_msgs
std_srvs
tf2_ros
yaml-cpp
@@ -35,6 +36,8 @@
launch
launch_ros
launch_xml
+
+
nexus_capabilities
nexus_motion_planner
nexus_rviz_plugins
@@ -42,6 +45,19 @@
nexus_workcell_orchestrator
nexus_zenoh_bridge_dds_vendor
+
+ rmf_building_map_tools
+ rmf_demos_fleet_adapter
+ rmf_building_sim_gz_plugins
+ rmf_robot_sim_gz_plugins
+ rmf_task_ros2
+ rmf_traffic_ros2
+ rmf_visualization
+
+
+ gz_sim_vendor
+ ros_gz_bridge
+
ament_cmake_catch2
ament_index_cpp
rcpputils
diff --git a/nexus_integration_tests/ros_testcase.py b/nexus_integration_tests/ros_testcase.py
index da827ab..e27db1a 100644
--- a/nexus_integration_tests/ros_testcase.py
+++ b/nexus_integration_tests/ros_testcase.py
@@ -24,6 +24,7 @@
import rclpy.node
from lifecycle_msgs.msg import State
from lifecycle_msgs.srv import GetState
+from rmf_fleet_msgs.msg import RobotState
T = TypeVar("T", bound="RosTestCase")
@@ -102,6 +103,14 @@ def wait_for_nodes(self, *nodes: Sequence[str]):
time.sleep(0.1)
undiscovered.difference_update(self.node.get_node_names())
+ async def wait_for_robot_state(self):
+ fut = rclpy.Future()
+ sub = self.node.create_subscription(
+ RobotState, "/robot_state", fut.set_result, 10
+ )
+ result = await fut
+ self.assertIsNotNone(result)
+
async def ros_sleep(self, secs: float):
"""
async sleep using ros timers
diff --git a/nexus_integration_tests/rviz/nexus_panel_rmf.rviz b/nexus_integration_tests/rviz/nexus_panel_rmf.rviz
new file mode 100644
index 0000000..d2cbafc
--- /dev/null
+++ b/nexus_integration_tests/rviz/nexus_panel_rmf.rviz
@@ -0,0 +1,251 @@
+Panels:
+ - Class: rviz_common/Displays
+ Help Height: 138
+ Name: Displays
+ Property Tree Widget:
+ Expanded:
+ - /Global Options1
+ - /Status1
+ - /MarkerArray1
+ - /MarkerArray1/Topic1
+ - /MarkerArray2
+ - /MarkerArray2/Topic1
+ - /MarkerArray3
+ - /MarkerArray3/Topic1
+ - /MarkerArray4
+ - /MarkerArray4/Topic1
+ - /Map1
+ - /Map1/Status1
+ - /Map1/Topic1
+ Splitter Ratio: 0.5
+ Tree Height: 226
+ - Class: rviz_common/Selection
+ Name: Selection
+ - Class: rviz_common/Tool Properties
+ Expanded:
+ - /2D Goal Pose1
+ - /Publish Point1
+ Name: Tool Properties
+ Splitter Ratio: 0.5886790156364441
+ - Class: rviz_common/Views
+ Expanded:
+ - /Current View1
+ Name: Views
+ Splitter Ratio: 0.5
+ - Class: rviz_common/Time
+ Experimental: false
+ Name: Time
+ SyncMode: 0
+ SyncSource: ""
+ - Class: nexus_rviz_plugins/NEXUSPanel
+ Name: NEXUSPanel
+ - Class: rmf_visualization_rviz2_plugins/SchedulePanel
+ Finish: 600
+ Map: L1
+ Name: SchedulePanel
+ Topic: /rmf_visualization/parameters
+Visualization Manager:
+ Class: ""
+ Displays:
+ - Alpha: 0.5
+ Cell Size: 1
+ Class: rviz_default_plugins/Grid
+ Color: 160; 160; 164
+ Enabled: true
+ Line Style:
+ Line Width: 0.029999999329447746
+ Value: Lines
+ Name: Grid
+ Normal Cell Count: 0
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Plane: XY
+ Plane Cell Count: 10
+ Reference Frame:
+ Value: true
+ - Class: rviz_default_plugins/TF
+ Enabled: true
+ Filter (blacklist): ""
+ Filter (whitelist): ""
+ Frame Timeout: 600
+ Frames:
+ All Enabled: true
+ deliveryRobot1/base_link:
+ Value: true
+ deliveryRobot2/base_link:
+ Value: true
+ world:
+ Value: true
+ Marker Scale: 1
+ Name: TF
+ Show Arrows: true
+ Show Axes: true
+ Show Names: true
+ Tree:
+ world:
+ deliveryRobot1/base_link:
+ {}
+ deliveryRobot2/base_link:
+ {}
+ Update Interval: 0
+ Value: true
+ - Class: rviz_default_plugins/MarkerArray
+ Enabled: true
+ Name: MarkerArray
+ Namespaces:
+ body: true
+ name: true
+ nose: true
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /fleet_markers
+ Value: true
+ - Class: rviz_default_plugins/MarkerArray
+ Enabled: true
+ Name: MarkerArray
+ Namespaces:
+ {}
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /schedule_markers
+ Value: true
+ - Class: rviz_default_plugins/MarkerArray
+ Enabled: true
+ Name: MarkerArray
+ Namespaces:
+ deliveryRobot/labels/L1: true
+ deliveryRobot/lanes/L1: true
+ deliveryRobot/waypoints/L1: true
+ Topic:
+ Depth: 5
+ Durability Policy: Transient Local
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /map_markers
+ Value: true
+ - Class: rviz_default_plugins/MarkerArray
+ Enabled: true
+ Name: MarkerArray
+ Namespaces:
+ {}
+ Topic:
+ Depth: 5
+ Durability Policy: Transient Local
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /building_systems_markers
+ Value: true
+ - Alpha: 0.699999988079071
+ Binary representation: false
+ Binary threshold: 100
+ Class: rviz_default_plugins/Map
+ Color Scheme: map
+ Draw Behind: false
+ Enabled: true
+ Name: Map
+ Topic:
+ Depth: 5
+ Durability Policy: Transient Local
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /floorplan
+ Update Topic:
+ Depth: 5
+ Durability Policy: Transient Loca
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /floorplan_updates
+ Use Timestamp: false
+ Value: true
+ Enabled: true
+ Global Options:
+ Background Color: 48; 48; 48
+ Fixed Frame: map
+ Frame Rate: 30
+ Name: root
+ Tools:
+ - Class: rviz_default_plugins/Interact
+ Hide Inactive Objects: true
+ - Class: rviz_default_plugins/MoveCamera
+ - Class: rviz_default_plugins/Select
+ - Class: rviz_default_plugins/FocusCamera
+ - Class: rviz_default_plugins/Measure
+ Line color: 128; 128; 0
+ - Class: rviz_default_plugins/SetInitialPose
+ Covariance x: 0.25
+ Covariance y: 0.25
+ Covariance yaw: 0.06853891909122467
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /initialpose
+ - Class: rviz_default_plugins/SetGoal
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /goal_pose
+ - Class: rviz_default_plugins/PublishPoint
+ Single click: true
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /clicked_point
+ Transformation:
+ Current:
+ Class: rviz_default_plugins/TF
+ Value: true
+ Views:
+ Current:
+ Angle: 0.005000006407499313
+ Class: rviz_default_plugins/TopDownOrtho
+ Enable Stereo Rendering:
+ Stereo Eye Separation: 0.05999999865889549
+ Stereo Focal Distance: 1
+ Swap Stereo Eyes: false
+ Value: false
+ Invert Z Axis: false
+ Name: Current View
+ Near Clip Distance: 0.009999999776482582
+ Scale: 30.049789428710938
+ Target Frame:
+ Value: TopDownOrtho (rviz_default_plugins)
+ X: 15.933613777160645
+ Y: -7.778347492218018
+ Saved: ~
+Window Geometry:
+ Displays:
+ collapsed: false
+ Height: 932
+ Hide Left Dock: false
+ Hide Right Dock: true
+ NEXUSPanel:
+ collapsed: false
+ QMainWindow State: 000000ff00000000fd00000004000000000000019400000304fc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005d00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afc0000003f000001ac000000cc00fffffffa000000010100000002fb000000120046004d004d005300500061006e0065006c0100000000ffffffff0000000000000000fb000000100044006900730070006c0061007900730100000000000001780000015600fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d0065007200610000000471000000e10000000000000000fb0000000a0049006d00610067006501000002e6000000950000000000000000fc000001f100000152000001120100001efa000000010100000002fb00000014004e004500580055005300500061006e0065006c010000000000000194000000e200fffffffb0000001a005300630068006500640075006c006500500061006e0065006c0100000000ffffffff0000019400ffffff000000010000012e0000033efc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003d0000033e000000a900fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004f20000003cfc0100000002fb0000000800540069006d00650100000000000004f20000027b00fffffffb0000000800540069006d00650100000000000004500000000000000000000003580000030400000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
+ SchedulePanel:
+ collapsed: false
+ Selection:
+ collapsed: false
+ Time:
+ collapsed: false
+ Tool Properties:
+ collapsed: false
+ Views:
+ collapsed: true
+ Width: 1266
+ X: 1081
+ Y: 67
diff --git a/nexus_integration_tests/test_pick_and_place_rmf.py b/nexus_integration_tests/test_pick_and_place_rmf.py
new file mode 100644
index 0000000..d117321
--- /dev/null
+++ b/nexus_integration_tests/test_pick_and_place_rmf.py
@@ -0,0 +1,114 @@
+# Copyright (C) 2025 Open Source Robotics Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+from typing import cast
+
+from action_msgs.msg import GoalStatus
+from managed_process import managed_process
+from nexus_orchestrator_msgs.action import ExecuteWorkOrder
+from nexus_orchestrator_msgs.msg import TaskState
+from nexus_test_case import NexusTestCase
+from rclpy import Future
+from rclpy.action import ActionClient
+from rclpy.action.client import ClientGoalHandle, GoalStatus
+from ros_testcase import RosTestCase
+import subprocess
+
+class PickAndPlaceTest(NexusTestCase):
+ @RosTestCase.timeout(60)
+ async def asyncSetUp(self):
+ # todo(YV): Find a better fix to the problem below.
+ # zenoh-bridge was bumped to 0.72 as part of the upgrade to
+ # ROS 2 Iron. However with this upgrade, the bridge does not clearly
+ # terminate when a SIGINT is received leaving behind zombie bridge
+ # processes from previous test cases. As a result, workcell registration
+ # fails for this testcase due to multiple bridges remaining active.
+ # Hence we explicitly kill any zenoh processes before launching the test.
+ subprocess.Popen('pkill -9 -f zenoh', shell=True)
+
+ self.proc = managed_process(
+ (
+ "ros2",
+ "launch",
+ "nexus_integration_tests",
+ "launch.py",
+ "sim_update_rate:=10000",
+ "use_rmf_transporter:=true"
+ ),
+ )
+ self.proc.__enter__()
+ print("waiting for nodes to be ready...", file=sys.stderr)
+ self.wait_for_nodes("system_orchestrator")
+ await self.wait_for_lifecycle_active("system_orchestrator")
+
+ await self.wait_for_workcells("workcell_1", "workcell_2", "rmf_nexus_transporter")
+ print("all workcells are ready")
+ await self.wait_for_robot_state()
+ print("AMRs are ready")
+
+ # give some time for discovery to happen
+ await self.ros_sleep(5)
+
+ # create action client to send work order
+ self.action_client = ActionClient(
+ self.node, ExecuteWorkOrder, "/system_orchestrator/execute_order"
+ )
+
+ def tearDown(self):
+ self.proc.__exit__(None, None, None)
+
+ @RosTestCase.timeout(600) # 10min
+ async def test_pick_and_place_wo(self):
+ self.action_client.wait_for_server()
+ goal_msg = ExecuteWorkOrder.Goal()
+ with open(f"{os.path.dirname(__file__)}/config/pick_and_place.json") as f:
+ goal_msg.order.work_order = f.read()
+ feedbacks: list[ExecuteWorkOrder.Feedback] = []
+ fb_fut = Future()
+
+ def on_fb(msg):
+ feedbacks.append(msg.feedback)
+ if len(feedbacks) >= 5:
+ fb_fut.set_result(None)
+
+ goal_handle = cast(
+ ClientGoalHandle, await self.action_client.send_goal_async(goal_msg, on_fb)
+ )
+ self.assertTrue(goal_handle.accepted)
+
+ results = await goal_handle.get_result_async()
+ self.assertEqual(results.status, GoalStatus.STATUS_SUCCEEDED)
+
+ # check that we receive the correct feedbacks
+ # FIXME(koonpeng): First few feedbacks are sometimes missed when the system in under
+ # high load so we only check the last feedback as a workaround.
+ self.assertGreater(len(feedbacks), 0)
+ for msg in feedbacks:
+ # The first task is transportation
+ self.assertEqual(len(msg.task_states), 3)
+ state: TaskState = msg.task_states[1] # type: ignore
+ self.assertEqual(state.workcell_id, "workcell_1")
+ self.assertEqual(state.task_id, "1")
+ state: TaskState = msg.task_states[2] # type: ignore
+ self.assertEqual(state.workcell_id, "workcell_2")
+ self.assertEqual(state.task_id, "2")
+
+ state: TaskState = feedbacks[-1].task_states[0] # type: ignore
+ self.assertEqual(state.status, TaskState.STATUS_FINISHED)
+ state: TaskState = feedbacks[-1].task_states[1] # type: ignore
+ self.assertEqual(state.status, TaskState.STATUS_FINISHED)
+ state: TaskState = feedbacks[-1].task_states[2] # type: ignore
+ self.assertEqual(state.status, TaskState.STATUS_FINISHED)
diff --git a/nexus_system_orchestrator/CMakeLists.txt b/nexus_system_orchestrator/CMakeLists.txt
index d3459bb..6749017 100644
--- a/nexus_system_orchestrator/CMakeLists.txt
+++ b/nexus_system_orchestrator/CMakeLists.txt
@@ -19,6 +19,7 @@ set(CMAKE_CXX_FLAGS "-Wall -Wpedantic")
add_library(${PROJECT_NAME}_plugin SHARED
src/bid_transporter.cpp
+ src/assign_transporter_workcell.cpp
src/execute_task.cpp
src/for_each_task.cpp
src/send_signal.cpp
diff --git a/nexus_system_orchestrator/src/assign_transporter_workcell.cpp b/nexus_system_orchestrator/src/assign_transporter_workcell.cpp
new file mode 100644
index 0000000..823fdce
--- /dev/null
+++ b/nexus_system_orchestrator/src/assign_transporter_workcell.cpp
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2025 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include
+
+#include
+
+#include "assign_transporter_workcell.hpp"
+
+namespace nexus::system_orchestrator {
+
+BT::PortsList AssignTransporterWorkcell::providedPorts()
+{
+ return {
+ BT::OutputPort("transporter_id"),
+ BT::OutputPort("transport_task")
+ };
+}
+
+BT::NodeStatus AssignTransporterWorkcell::onStart()
+{
+ auto node = this->_w_node.lock();
+ if (!node)
+ {
+ std::cerr <<
+ "FATAL ERROR!!! NODE IS DESTROYED WHILE THERE ARE STILL REFERENCES" <<
+ std::endl;
+ std::terminate();
+ }
+
+ // Create a list of destination from the current context tasks
+ const auto& task_assignments = this->_ctx->workcell_task_assignments;
+ YAML::Node orders;
+ std::vector locations;
+
+ for (const auto& task : this->_ctx->tasks)
+ {
+ auto assignment_it = task_assignments.find(task.id);
+ if (assignment_it == task_assignments.end())
+ {
+ RCLCPP_ERROR(
+ node->get_logger(), "%s: Unable to transport, task [%s] was not assigned to a workcell",
+ this->name().c_str(), task.id.c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+ // Multipickup task
+ // TODO(luca) Consider encoding where is a pickup and where a dropoff
+ // TODO(luca) remove consecutive duplicates (multiple tasks to same workcell that don't need transportation)
+ YAML::Node order;
+ order["type"] = "pickup";
+ order["destination"] = assignment_it->second;
+ order["workcell_task_id"] = task.id;
+ orders.push_back(order);
+ }
+ this->_transport_task.id = this->_ctx->job_id;
+ this->_transport_task.type = "transportation";
+ YAML::Emitter out;
+ out << orders;
+ this->_transport_task.payload = out.c_str();
+
+ // Probe workcells for transportation capability to this set of destinations
+ auto req =
+ std::make_shared();
+ req->task = this->_transport_task;
+ // send request to all transporters in parallel
+ for (auto& [workcell_id, session] : this->_ctx->workcell_sessions)
+ {
+ auto fut = session->task_doable_client->async_send_request(req);
+ this->_ongoing_requests.emplace(workcell_id,
+ OngoingRequest{session->task_doable_client, std::move(fut)});
+ }
+ this->_time_limit = std::chrono::steady_clock::now() +
+ std::chrono::seconds{5};
+
+ return BT::NodeStatus::RUNNING;
+}
+
+BT::NodeStatus AssignTransporterWorkcell::onRunning()
+{
+ auto result = this->_update_ongoing_requests();
+ if (result != BT::NodeStatus::RUNNING)
+ {
+ // action is done, drop requests that have not received response to avoid callback leak.
+ this->_cleanup_pending_requests();
+ }
+ return result;
+}
+
+void AssignTransporterWorkcell::onHalted()
+{
+ // can't cancel a service call, so we just drop the requests.
+ this->_cleanup_pending_requests();
+}
+
+void AssignTransporterWorkcell::_cleanup_pending_requests()
+{
+ for (auto& [_, ongoing_req] : this->_ongoing_requests)
+ {
+ ongoing_req.client->remove_pending_request(ongoing_req.fut);
+ }
+}
+
+BT::NodeStatus AssignTransporterWorkcell::_update_ongoing_requests()
+{
+ auto node = this->_w_node.lock();
+ if (!node)
+ {
+ std::cerr <<
+ "FATAL ERROR!!! NODE IS DESTROYED WHILE THERE ARE STILL REFERENCES" <<
+ std::endl;
+ std::terminate();
+ }
+
+ if (std::chrono::steady_clock::now() > this->_time_limit)
+ {
+ for (const auto& [workcell_id, _] : this->_ongoing_requests)
+ {
+ RCLCPP_WARN(
+ node->get_logger(), "%s: Skipped transporter [%s] (no response)",
+ this->name().c_str(),
+ workcell_id.c_str());
+ }
+ RCLCPP_ERROR(
+ node->get_logger(), "%s: No transporter is able to perform task",
+ this->name().c_str());
+ return BT::NodeStatus::FAILURE;
+ }
+
+ std::vector finished_requests;
+ for (auto& [workcell_id, ongoing_req] : this->_ongoing_requests)
+ {
+ if (ongoing_req.fut.wait_for(std::chrono::seconds{0}) ==
+ std::future_status::ready)
+ {
+ finished_requests.emplace_back(workcell_id);
+ auto resp = ongoing_req.fut.get();
+ // TODO(luca) make this proper bidding
+ // use first available transporter
+ if (resp->success)
+ {
+ RCLCPP_INFO(
+ node->get_logger(), "[%s]: Bid awarded to [%s]",
+ this->name().c_str(),
+ workcell_id.c_str());
+ this->setOutput("transporter_id", workcell_id);
+ this->setOutput("transport_task", this->_transport_task);
+ // Update the context
+ const auto& task_id = this->_transport_task.id;
+ this->_ctx->workcell_task_assignments.emplace(task_id, workcell_id);
+ auto p = this->_ctx->task_states.emplace(task_id, nexus_orchestrator_msgs::msg::TaskState());
+ auto& task_state = p.first->second;
+ task_state.workcell_id = workcell_id;
+ task_state.task_id = task_id;
+ return BT::NodeStatus::SUCCESS;
+ }
+ else
+ {
+ RCLCPP_DEBUG(
+ node->get_logger(), "%s: Transporter [%s] cannot perform task",
+ this->name().c_str(), workcell_id.c_str());
+ }
+ }
+ }
+
+ for (const auto& workcell_id : finished_requests)
+ {
+ this->_ongoing_requests.erase(workcell_id);
+ }
+
+ return BT::NodeStatus::RUNNING;
+}
+
+}
diff --git a/nexus_system_orchestrator/src/assign_transporter_workcell.hpp b/nexus_system_orchestrator/src/assign_transporter_workcell.hpp
new file mode 100644
index 0000000..2c83767
--- /dev/null
+++ b/nexus_system_orchestrator/src/assign_transporter_workcell.hpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2025 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef NEXUS_SYSTEM_ORCHESTRATOR__DISPATCH_TRANSPORTER_HPP
+#define NEXUS_SYSTEM_ORCHESTRATOR__DISPATCH_TRANSPORTER_HPP
+
+#include "context.hpp"
+#include "session.hpp"
+
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+
+namespace nexus::system_orchestrator {
+
+/**
+ * Iterates over all the assigned workcell tasks (from work order steps) and bid + assign a single
+ * transporter workcell to move material to the other workcells workcells.
+ *
+ * Output Ports:
+ * transporter_id |std::string| ID of the transporter workcell assigned to move
+ * material between all other workcells.
+ * transport_task |WorkcellTask| The WorkcellTask definition for the transporter workcell to execute.
+ */
+class AssignTransporterWorkcell : public BT::StatefulActionNode
+{
+public: using IsTaskDoableService =
+ endpoints::IsTaskDoableService;
+
+public: using WorkcellTask =
+ nexus_orchestrator_msgs::msg::WorkcellTask;
+
+public: static BT::PortsList providedPorts();
+
+public: AssignTransporterWorkcell(const std::string& name,
+ const BT::NodeConfiguration& config,
+ rclcpp_lifecycle::LifecycleNode::WeakPtr w_node,
+ std::shared_ptr ctx)
+ : BT::StatefulActionNode{name, config}, _w_node{w_node}, _ctx{std::move(ctx)}
+ {
+ }
+
+public: BT::NodeStatus onStart() override;
+public: BT::NodeStatus onRunning() override;
+public: void onHalted() override;
+
+private: struct OngoingRequest
+ {
+ rclcpp::Client::SharedPtr client;
+ rclcpp::Client::
+ FutureAndRequestId fut;
+ };
+
+private: rclcpp_lifecycle::LifecycleNode::WeakPtr _w_node;
+private: std::shared_ptr _ctx;
+private: std::unordered_map _ongoing_requests;
+private: std::chrono::steady_clock::time_point _time_limit;
+private: WorkcellTask _transport_task;
+
+private: void _cleanup_pending_requests();
+private: BT::NodeStatus _update_ongoing_requests();
+};
+
+}
+
+#endif
diff --git a/nexus_system_orchestrator/src/context.hpp b/nexus_system_orchestrator/src/context.hpp
index 3f0f719..bbf8753 100644
--- a/nexus_system_orchestrator/src/context.hpp
+++ b/nexus_system_orchestrator/src/context.hpp
@@ -66,7 +66,7 @@ public: std::vector errors;
public: std::string task_results;
/**
- * Map of task ids and their queued signals.
+ * Map of workcell task ids and their queued signals.
*/
public: std::unordered_map> queued_signals;
diff --git a/nexus_system_orchestrator/src/execute_task.cpp b/nexus_system_orchestrator/src/execute_task.cpp
index f3ce222..c3cca54 100644
--- a/nexus_system_orchestrator/src/execute_task.cpp
+++ b/nexus_system_orchestrator/src/execute_task.cpp
@@ -65,6 +65,11 @@ BT::NodeStatus ExecuteTask::onStart()
task_bt_path));
this->_bt->rootBlackboard()->set("task", *task);
this->_bt->rootBlackboard()->set("workcell", *workcell);
+ auto transport_task = this->getInput("transport_task");
+ if (transport_task)
+ {
+ this->_bt->rootBlackboard()->set("transport_task", *transport_task);
+ }
return BT::NodeStatus::RUNNING;
}
diff --git a/nexus_system_orchestrator/src/execute_task.hpp b/nexus_system_orchestrator/src/execute_task.hpp
index c3ce603..63cfc94 100644
--- a/nexus_system_orchestrator/src/execute_task.hpp
+++ b/nexus_system_orchestrator/src/execute_task.hpp
@@ -46,7 +46,8 @@ public: using Task = nexus_orchestrator_msgs::msg::WorkcellTask;
public: static BT::PortsList providedPorts()
{
return { BT::InputPort("task"),
- BT::InputPort("workcell") };
+ BT::InputPort("workcell"),
+ BT::InputPort("transport_task") };
}
public: ExecuteTask(const std::string& name,
diff --git a/nexus_system_orchestrator/src/system_orchestrator.cpp b/nexus_system_orchestrator/src/system_orchestrator.cpp
index 5977a74..423c558 100644
--- a/nexus_system_orchestrator/src/system_orchestrator.cpp
+++ b/nexus_system_orchestrator/src/system_orchestrator.cpp
@@ -19,6 +19,7 @@
#include "bid_transporter.hpp"
#include "context.hpp"
+#include "assign_transporter_workcell.hpp"
#include "exceptions.hpp"
#include "execute_task.hpp"
#include "for_each_task.hpp"
@@ -501,6 +502,13 @@ BT::Tree SystemOrchestrator::_create_bt(const WorkOrderActionType::Goal& wo,
});
});
+ bt_factory->registerBuilder("AssignTransporterWorkcell",
+ [this, ctx](const std::string& name, const BT::NodeConfiguration& config)
+ {
+ return std::make_unique(name, config,
+ this->shared_from_this(), ctx);
+ });
+
bt_factory->registerBuilder("BidTransporter",
[this, ctx](const std::string& name, const BT::NodeConfiguration& config)
{
@@ -852,7 +860,7 @@ void SystemOrchestrator::_halt_job(const std::string& job_id)
std::make_shared();
req->task_id = task_id;
const auto resp =
- this->_workcell_sessions.at(task_id)->remove_pending_task_client->
+ this->_workcell_sessions.at(wc_id)->remove_pending_task_client->
send_request(req);
if (!resp->success)
{
diff --git a/nexus_workcell_orchestrator/src/workcell_orchestrator.cpp b/nexus_workcell_orchestrator/src/workcell_orchestrator.cpp
index 3e1b4e3..4c03e87 100644
--- a/nexus_workcell_orchestrator/src/workcell_orchestrator.cpp
+++ b/nexus_workcell_orchestrator/src/workcell_orchestrator.cpp
@@ -955,6 +955,7 @@ void WorkcellOrchestrator::_process_signal(
req->signal.c_str(), req->task_id.c_str());
ctx->signals.emplace(req->signal);
resp->success = true;
+ return;
}
}