SMACC2
Loading...
Searching...
No Matches
cp_forward_obstacle_guard.cpp
Go to the documentation of this file.
1// Copyright 2026 RobosoftAI Inc.
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/*****************************************************************************************************************
16 *
17 * Authors: Brett Aldrich
18 *
19 ******************************************************************************************************************/
20
22
23#include <tf2/LinearMath/Matrix3x3.h>
24#include <tf2/LinearMath/Quaternion.h>
25#include <sensor_msgs/point_cloud2_iterator.hpp>
26
27#include <limits>
28
29namespace cl_px4_mr
30{
31
33: params_(params), cosHalfAngle_(std::cos(params.halfAngleRad))
34{
35}
36
38
40{
42 if (subscriber_ == nullptr)
43 {
44 RCLCPP_ERROR(
45 getLogger(),
46 "CpForwardObstacleGuard: no CpTopicSubscriber<PointCloud2> on this client - guard inactive");
47 return;
48 }
51 {
52 attitudeSub_ = this->getNode()->create_subscription<px4_msgs::msg::VehicleAttitude>(
53 "/fmu/out/vehicle_attitude", rclcpp::SensorDataQoS(),
54 std::bind(&CpForwardObstacleGuard::onAttitude, this, std::placeholders::_1));
55 }
56 RCLCPP_INFO(
57 getLogger(),
58 "CpForwardObstacleGuard: subscribed - cone %.0f deg, trigger < %.1f m, clear > %.1f m, "
59 "%d hits, %d/%d clouds, yaw-rate gate %.2f rad/s, floor cutoff %.1f m, level frame %s",
62 params_.floorCutoffM, params_.levelFrame ? "on" : "off");
63}
64
66{
67 const bool was = tooClose_.exchange(false);
69 clearStreak_ = 0;
70 if (was)
71 {
72 RCLCPP_INFO(
73 getLogger(), "CpForwardObstacleGuard: reset (stale flag dropped, last min range %.2f m)",
74 lastMinRange_.load());
75 }
76}
77
78void CpForwardObstacleGuard::onAttitude(const px4_msgs::msg::VehicleAttitude::SharedPtr msg)
79{
80 // yaw rate by finite difference of the NED heading (PX4 does not bridge
81 // vehicle_angular_velocity by default); light EMA against quantisation
82 const double w = msg->q[0], x = msg->q[1], y = msg->q[2], z = msg->q[3];
83 const double yaw = std::atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z));
84 if (lastYawStampUs_ != 0 && msg->timestamp > lastYawStampUs_)
85 {
86 const double dt = static_cast<double>(msg->timestamp - lastYawStampUs_) * 1e-6;
87 double d = yaw - lastYaw_;
88 d = std::atan2(std::sin(d), std::cos(d));
89 if (dt > 1e-4 && dt < 0.5)
90 {
91 const float rate = static_cast<float>(d / dt);
92 yawRate_ = 0.5f * yawRate_.load() + 0.5f * rate;
93 }
94 }
95 lastYaw_ = yaw;
96 lastYawStampUs_ = msg->timestamp;
97
98 std::lock_guard<std::mutex> lock(attitudeMutex_);
99 attitudeQ_ = {msg->q[0], msg->q[1], msg->q[2], msg->q[3]};
100 haveAttitude_ = true;
101}
102
103bool CpForwardObstacleGuard::levelRotation(std::array<float, 9> & r) const
104{
105 std::array<float, 4> q;
106 {
107 std::lock_guard<std::mutex> lock(attitudeMutex_);
108 if (!haveAttitude_)
109 {
110 return false;
111 }
112 q = attitudeQ_;
113 }
114 // body FLU -> ENU (px4_ros_com constants), then strip the yaw so the result
115 // is the roll/pitch-only rotation into a level, heading-aligned frame
116 const tf2::Quaternion kNedEnuQ(0.70710678118654752, 0.70710678118654752, 0.0, 0.0);
117 const tf2::Quaternion kFrdFluQ(1.0, 0.0, 0.0, 0.0);
118 const tf2::Quaternion qNedFrd(q[1], q[2], q[3], q[0]);
119 tf2::Quaternion qEnuFlu = kNedEnuQ * qNedFrd * kFrdFluQ;
120 qEnuFlu.normalize();
121 const tf2::Matrix3x3 m(qEnuFlu);
122 const double yaw = std::atan2(m[1][0], m[0][0]);
123 tf2::Matrix3x3 unyaw;
124 unyaw.setRPY(0.0, 0.0, -yaw);
125 const tf2::Matrix3x3 level = unyaw * m;
126 for (int i = 0; i < 3; ++i)
127 {
128 for (int j = 0; j < 3; ++j)
129 {
130 r[i * 3 + j] = static_cast<float>(level[i][j]);
131 }
132 }
133 return true;
134}
135
136void CpForwardObstacleGuard::onCloud(const sensor_msgs::msg::PointCloud2 & msg)
137{
138 if (params_.maxYawRateRadS > 0.0f && std::fabs(yawRate_.load()) > params_.maxYawRateRadS)
139 {
140 // turning: the cone is sweeping sideways; keep the current flag, count nothing
141 if (++gatedClouds_ == 1)
142 {
143 RCLCPP_INFO(
144 getLogger(), "CpForwardObstacleGuard: yawing at %.2f rad/s - cone gated", yawRate_.load());
145 }
146 return;
147 }
148 if (gatedClouds_ > 0)
149 {
150 RCLCPP_INFO(
151 getLogger(), "CpForwardObstacleGuard: cone active again after %d gated clouds", gatedClouds_);
152 gatedClouds_ = 0;
153 triggerStreak_ = 0;
154 }
155
156 int hits = 0;
157 float minRange = std::numeric_limits<float>::infinity();
158
159 std::array<float, 9> r{};
160 const bool level = params_.levelFrame && levelRotation(r);
161
162 sensor_msgs::PointCloud2ConstIterator<float> ix(msg, "x");
163 sensor_msgs::PointCloud2ConstIterator<float> iy(msg, "y");
164 sensor_msgs::PointCloud2ConstIterator<float> iz(msg, "z");
165 for (; ix != ix.end(); ++ix, ++iy, ++iz)
166 {
167 float x = *ix;
168 float y = *iy;
169 float z = *iz;
170 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
171 {
172 continue; // gz reports no-return as inf (is_dense = false)
173 }
174 if (level)
175 {
176 const float lx = r[0] * x + r[1] * y + r[2] * z;
177 const float ly = r[3] * x + r[4] * y + r[5] * z;
178 const float lz = r[6] * x + r[7] * y + r[8] * z;
179 x = lx;
180 y = ly;
181 z = lz;
182 }
183 if (params_.floorCutoffM > 0.0f && z < -params_.floorCutoffM)
184 {
185 continue; // the floor, not something in the way
186 }
187 const float r = std::sqrt(x * x + y * y + z * z);
188 if (r < params_.minRangeM || r > params_.clearRangeM)
189 {
190 continue;
191 }
192 if (x / r < cosHalfAngle_)
193 {
194 continue; // outside the forward cone
195 }
196 if (r < minRange)
197 {
198 minRange = r;
199 }
200 if (r < params_.triggerRangeM)
201 {
202 ++hits;
203 }
204 }
205
206 lastMinRange_ = minRange;
207 lastHits_ = hits;
208
209 const bool triggering = hits >= params_.minHits;
210 // cleared only when nothing in the cone is below the clear range at all
211 const bool clear = !(minRange < params_.clearRangeM);
212
213 if (!tooClose_)
214 {
215 triggerStreak_ = triggering ? triggerStreak_ + 1 : 0;
217 {
218 tooClose_ = true;
219 clearStreak_ = 0;
220 RCLCPP_WARN(
221 getLogger(), "CpForwardObstacleGuard: OBSTACLE min range %.2f m (%d hits in cone)",
222 minRange, hits);
224 }
225 }
226 else
227 {
228 clearStreak_ = clear ? clearStreak_ + 1 : 0;
230 {
231 tooClose_ = false;
232 triggerStreak_ = 0;
233 RCLCPP_INFO(getLogger(), "CpForwardObstacleGuard: cleared (min range %.2f m)", minRange);
235 }
236 }
237}
238
239} // namespace cl_px4_mr
smacc2::client_core_components::CpTopicSubscriber< sensor_msgs::msg::PointCloud2 > * subscriber_
CpForwardObstacleGuard(ForwardObstacleGuardParams params={})
void onAttitude(const px4_msgs::msg::VehicleAttitude::SharedPtr msg)
bool levelRotation(std::array< float, 9 > &r) const
void onCloud(const sensor_msgs::msg::PointCloud2 &msg)
smacc2::SmaccSignal< void()> onObstacleCleared_
rclcpp::Subscription< px4_msgs::msg::VehicleAttitude >::SharedPtr attitudeSub_
smacc2::SmaccSignal< void()> onObstacleTooClose_
rclcpp::Logger getLogger() const
void requiresComponent(TComponent *&requiredComponentStorage, ComponentRequirement requirementType=ComponentRequirement::SOFT)
rclcpp::Node::SharedPtr getNode()
smacc2::SmaccSignalConnection onMessageReceived(void(T::*callback)(const MessageType &), T *object)