AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
send_vehicle_command.cpp
1// Copyright 2024 Robin Müller
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "auto_apms_px4/node/send_vehicle_command.hpp"
16
17#include <algorithm>
18
19#include "px4_ros2/utils/message_version.hpp"
20
21// AUTO_APMS_PX4_SOURCE_COMPONENT_GLOBAL_KEY is injected as a compile definition (see CMakeLists.txt). It is the
22// global blackboard entry an in-charge auto_apms_px4::BehaviorModeExecutor publishes its VehicleCommand source
23// component under. The '@' prefix accesses the root (global) blackboard transitively, regardless of the (sub)tree the
24// node lives in.
25#ifndef AUTO_APMS_PX4_SOURCE_COMPONENT_GLOBAL_KEY
26#error "AUTO_APMS_PX4_SOURCE_COMPONENT_GLOBAL_KEY must be defined (see CMakeLists.txt)"
27#endif
28#ifndef AUTO_APMS_PX4_MODE_EXECUTOR_ACTIVE_GLOBAL_KEY
29#error "AUTO_APMS_PX4_MODE_EXECUTOR_ACTIVE_GLOBAL_KEY must be defined (see CMakeLists.txt)"
30#endif
31
32namespace auto_apms_px4
33{
34
35SendVehicleCommand::AckSubscription::AckSubscription(
36 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & topic_name)
37: name(topic_name)
38{
39 rclcpp::SubscriptionOptions options;
40 options.callback_group = group;
41 // The single subscription callback fans every acknowledgement out to all the command nodes that registered a
42 // listener. Each listener keeps its own matching state (see SendVehicleCommand's listener below).
43 subscription = node->create_subscription<Ack>(
44 topic_name, rclcpp::SensorDataQoS(), [this](Ack::SharedPtr msg) { broadcast(msg); }, options);
45}
46
47void SendVehicleCommand::AckSubscription::addListener(
48 const void * owner, std::function<void(const Ack::SharedPtr &)> callback)
49{
50 listeners.emplace_back(owner, std::move(callback));
51}
52
53void SendVehicleCommand::AckSubscription::removeListener(const void * owner)
54{
55 listeners.erase(
56 std::remove_if(listeners.begin(), listeners.end(), [owner](const auto & pair) { return pair.first == owner; }),
57 listeners.end());
58}
59
60void SendVehicleCommand::AckSubscription::broadcast(const Ack::SharedPtr & msg)
61{
62 for (const auto & [owner, callback] : listeners) callback(msg);
63}
64
65SendVehicleCommand::SendVehicleCommand(
66 const std::string & instance_name, const Config & config, const Context & context)
67: RosActionNodeBase(instance_name, config, context)
68{
69 // The base class stores the context and logger and applies the node manifest 'port_alias' feature.
70 const rclcpp::Node::SharedPtr node = context_.getRosNode();
71 const rclcpp::CallbackGroup::SharedPtr group = context_.getWaitablesCallbackGroup();
72
73 // Publisher for the command. The topic is resolved here rather than fixed in the manifest: its base depends on the
74 // mode-executor flag on the global blackboard (available at construction) and its message version suffix is resolved
75 // at runtime. A manifest 'topic' still takes precedence (see resolveCommandTopicName). Obtained from the RosNodeBase
76 // shared-entity registry so command nodes publishing on the same topic reuse a single publisher.
77 const std::string command_topic = resolveCommandTopicName();
78 command_pub_ = getSharedEntity<rclcpp::Publisher<px4_msgs::msg::VehicleCommand>>(
79 command_topic, [&] { return node->create_publisher<px4_msgs::msg::VehicleCommand>(command_topic, 10); });
80
81 // Subscription to the command acknowledgements so we can confirm PX4 accepted the command. All command nodes share a
82 // single subscription (from the shared-entity registry) that broadcasts every ack to the per-node listeners; it is
83 // created up front (before any command is sent) so no acknowledgement is missed. Our listener only latches
84 // acknowledgements for the command currently in flight, and only while we are waiting - this correlates the ack to
85 // the send without any timestamp bookkeeping (there is no spin between arming the matcher and publishing, so a stale
86 // ack cannot slip in).
87 const std::string ack_topic =
88 "fmu/out/vehicle_command_ack" + px4_ros2::getMessageNameVersion<px4_msgs::msg::VehicleCommandAck>();
89 ack_sub_ = getSharedEntity<AckSubscription>(
90 ack_topic, [&] { return std::make_shared<AckSubscription>(node, group, ack_topic); });
91 ack_sub_->addListener(this, [this](const px4_msgs::msg::VehicleCommandAck::SharedPtr & msg) {
92 if (waiting_for_ack_ && msg->command == tx_msg_.command) last_ack_ = msg;
93 });
94
95 // Timeout is inferred from the `request_timeout` field of the node registration options.
96 ack_timeout_s_ = std::chrono::duration<double>(context_.getRegistrationOptions().request_timeout).count();
97
98 RCLCPP_DEBUG(
99 logger_, "%s - Using command publisher on '%s' and acknowledgement subscription on '%s'.",
100 context_.getFullyQualifiedTreeNodeName(this).c_str(), command_pub_->get_topic_name(),
101 ack_sub_->subscription->get_topic_name());
102}
103
104SendVehicleCommand::~SendVehicleCommand()
105{
106 // Deregister our listener so the shared subscription doesn't invoke a dangling callback after we are destroyed.
107 if (ack_sub_) ack_sub_->removeListener(this);
108}
109
110std::string SendVehicleCommand::resolveCommandTopicName()
111{
112 // A manifest-configured topic takes precedence (supports the node manifest 'topic' feature, incl. (input:port)
113 // substitutions).
114 if (const BT::Expected<std::string> expected = context_.getTopicName(this); expected && !expected.value().empty()) {
115 return expected.value();
116 }
117
118 // While a BehaviorModeExecutor is in charge it sets this flag on the global blackboard; route the command through
119 // the mode-executor command topic so PX4 attributes it to the executor. Otherwise use the default external command
120 // topic. The '@'-prefixed key resolves transitively to the root (global) blackboard.
121 bool via_mode_executor = false;
122 (void)config().blackboard->get<bool>(AUTO_APMS_PX4_MODE_EXECUTOR_ACTIVE_GLOBAL_KEY, via_mode_executor);
123 const std::string base = via_mode_executor ? "fmu/in/vehicle_command_mode_executor" : "fmu/in/vehicle_command";
124
125 // The message version suffix is resolved at runtime from the message definition rather than baked into the manifest.
126 return base + px4_ros2::getMessageNameVersion<px4_msgs::msg::VehicleCommand>();
127}
128
129BT::PortsList SendVehicleCommand::providedPorts()
130{
131 return {
132 BT::InputPort<int>(PORT_KEY_COMMAND, "VehicleCommand command id (see px4_msgs/msg/VehicleCommand)."),
133 BT::InputPort<double>(PORT_KEY_PARAM1, 0.0, "Command parameter 1."),
134 BT::InputPort<double>(PORT_KEY_PARAM2, 0.0, "Command parameter 2."),
135 BT::InputPort<double>(PORT_KEY_PARAM3, 0.0, "Command parameter 3."),
136 BT::InputPort<double>(PORT_KEY_PARAM4, 0.0, "Command parameter 4."),
137 BT::InputPort<double>(PORT_KEY_PARAM5, 0.0, "Command parameter 5."),
138 BT::InputPort<double>(PORT_KEY_PARAM6, 0.0, "Command parameter 6."),
139 BT::InputPort<double>(PORT_KEY_PARAM7, 0.0, "Command parameter 7."),
140 BT::InputPort<int>(PORT_KEY_CONFIRMATION, 0, "Confirmation count (0 = first transmission of this command)."),
141 BT::InputPort<int>(PORT_KEY_TARGET_SYSTEM, 0, "System that should execute the command."),
142 BT::InputPort<int>(PORT_KEY_TARGET_COMPONENT, 0, "Component that should execute the command (0 = all)."),
143 };
144}
145
146bool SendVehicleCommand::setMessage(px4_msgs::msg::VehicleCommand & msg)
147{
148 const BT::Expected<int> expected_command = getInput<int>(PORT_KEY_COMMAND);
149 if (!expected_command) {
150 RCLCPP_ERROR(
151 logger_, "%s - Missing required input '%s': %s", context_.getFullyQualifiedTreeNodeName(this).c_str(),
152 PORT_KEY_COMMAND, expected_command.error().c_str());
153 return false;
154 }
155
156 msg = px4_msgs::msg::VehicleCommand{};
157 msg.command = static_cast<uint32_t>(expected_command.value());
158 msg.param1 = static_cast<float>(getInput<double>(PORT_KEY_PARAM1).value_or(0.0));
159 msg.param2 = static_cast<float>(getInput<double>(PORT_KEY_PARAM2).value_or(0.0));
160 msg.param3 = static_cast<float>(getInput<double>(PORT_KEY_PARAM3).value_or(0.0));
161 msg.param4 = static_cast<float>(getInput<double>(PORT_KEY_PARAM4).value_or(0.0));
162 msg.param5 = getInput<double>(PORT_KEY_PARAM5).value_or(0.0);
163 msg.param6 = getInput<double>(PORT_KEY_PARAM6).value_or(0.0);
164 msg.param7 = static_cast<float>(getInput<double>(PORT_KEY_PARAM7).value_or(0.0));
165 msg.confirmation = static_cast<uint8_t>(getInput<int>(PORT_KEY_CONFIRMATION).value_or(0));
166 msg.target_system = static_cast<uint8_t>(getInput<int>(PORT_KEY_TARGET_SYSTEM).value_or(0));
167 msg.target_component = static_cast<uint8_t>(getInput<int>(PORT_KEY_TARGET_COMPONENT).value_or(0));
168 msg.source_system = 0;
169 msg.source_component = resolveSourceComponent();
170 msg.from_external = true;
171 msg.timestamp = 0; // Let PX4 set the timestamp.
172
173 return true;
174}
175
177{
178 // Transitively resolve the executor's source component from the global blackboard. If undefined, fall back to 0 (a
179 // normal external command) without modifying the blackboard.
180 int source_component = 0;
181 (void)config().blackboard->get<int>(AUTO_APMS_PX4_SOURCE_COMPONENT_GLOBAL_KEY, source_component);
182 return static_cast<uint16_t>(source_component);
183}
184
185BT::NodeStatus SendVehicleCommand::tick()
186{
187 if (!rclcpp::ok()) {
188 halt();
189 return BT::NodeStatus::FAILURE;
190 }
191
192 // Phase 1 (first tick, status IDLE): build and publish the command, then start waiting for the acknowledgement.
193 if (status() == BT::NodeStatus::IDLE) {
194 setStatus(BT::NodeStatus::RUNNING);
195
196 if (!setMessage(tx_msg_)) return BT::NodeStatus::FAILURE;
197
198 last_ack_.reset();
199 waiting_for_ack_ = true;
200 send_time_ = context_.getCurrentTime();
201 command_pub_->publish(tx_msg_);
202
203 RCLCPP_DEBUG(
204 logger_, "%s - Sent VehicleCommand %u and waiting for acknowledgement (timeout %.2fs).",
205 context_.getFullyQualifiedTreeNodeName(this).c_str(), tx_msg_.command, ack_timeout_s_);
206
207 return BT::NodeStatus::RUNNING;
208 }
209
210 // Phase 2 (subsequent ticks, status RUNNING): wait for the matching acknowledgement or time out.
211 if (last_ack_) {
212 const uint8_t result = last_ack_->result;
213 waiting_for_ack_ = false;
214 using Ack = px4_msgs::msg::VehicleCommandAck;
215 if (result == Ack::VEHICLE_CMD_RESULT_ACCEPTED || result == Ack::VEHICLE_CMD_RESULT_IN_PROGRESS) {
216 RCLCPP_DEBUG(
217 logger_, "%s - VehicleCommand %u accepted (result %u).", context_.getFullyQualifiedTreeNodeName(this).c_str(),
218 tx_msg_.command, result);
219 return BT::NodeStatus::SUCCESS;
220 }
221 RCLCPP_ERROR(
222 logger_, "%s - VehicleCommand %u was not accepted (result %u).",
223 context_.getFullyQualifiedTreeNodeName(this).c_str(), tx_msg_.command, result);
224 return BT::NodeStatus::FAILURE;
225 }
226
227 if ((context_.getCurrentTime() - send_time_).seconds() > ack_timeout_s_) {
228 waiting_for_ack_ = false;
229 RCLCPP_ERROR(
230 logger_, "%s - Timed out after %.2fs waiting for acknowledgement of VehicleCommand %u.",
231 context_.getFullyQualifiedTreeNodeName(this).c_str(), ack_timeout_s_, tx_msg_.command);
232 return BT::NodeStatus::FAILURE;
233 }
234
235 return BT::NodeStatus::RUNNING;
236}
237
238void SendVehicleCommand::halt()
239{
240 // Stop waiting and drop any pending acknowledgement, then return to IDLE so a later tick starts a fresh send.
241 waiting_for_ack_ = false;
242 last_ack_.reset();
243 resetStatus();
244}
245
246} // namespace auto_apms_px4
247
248// This translation unit is compiled into two libraries (see CMakeLists.txt): the exported auto_apms_px4 library, which
249// provides the SendVehicleCommand implementation to other targets (e.g. the generated node model header), and the
250// auto_apms_px4_behavior_tree_nodes pluginlib plugin. Only the plugin library must export the pluginlib factory.
251// Exporting it from the exported library as well would register the same factory a second time whenever a process links
252// that library directly (e.g. the behavior_mode_executor executable) in addition to loading the plugin, which triggers
253// a class_loader namespace-collision warning. AUTO_APMS_PX4_EXPORT_NODE_PLUGIN is defined only on the plugin target.
254#ifdef AUTO_APMS_PX4_EXPORT_NODE_PLUGIN
256#endif
Generic behavior tree node that publishes a PX4 VehicleCommand and waits for its acknowledgement.
uint16_t resolveSourceComponent()
Transitively resolve the in-charge mode executor's source component from the global blackboard.
virtual bool setMessage(px4_msgs::msg::VehicleCommand &msg)
Populate the VehicleCommand message to publish.
#define AUTO_APMS_BEHAVIOR_TREE_REGISTER_NODE(type)
Macro for registering a behavior tree node plugin.
Definition node.hpp:50
Implementation of PX4 mode peers offered by px4_ros2_cpp enabling integration with AutoAPMS.