SMACC2
Loading...
Searching...
No Matches
cp_tunnel_centering.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
23
24#include <sensor_msgs/point_cloud2_iterator.hpp>
25
26#include <algorithm>
27#include <cmath>
28#include <limits>
29#include <vector>
30
31namespace cl_px4_mr
32{
33
34namespace
35{
36constexpr float kInf = std::numeric_limits<float>::infinity();
37
38// k-th smallest of v (v is modified); +inf when v has fewer than k elements
39float kthSmallest(std::vector<float> & v, size_t k)
40{
41 if (v.size() < k || k == 0)
42 {
43 return kInf;
44 }
45 std::nth_element(v.begin(), v.begin() + (k - 1), v.end());
46 return v[k - 1];
47}
48
49int64_t nowNs()
50{
51 return std::chrono::duration_cast<std::chrono::nanoseconds>(
52 std::chrono::steady_clock::now().time_since_epoch())
53 .count();
54}
55} // namespace
56
58
60
62{
64 if (subscriber_ == nullptr)
65 {
66 RCLCPP_ERROR(
67 getLogger(),
68 "CpTunnelCentering: no CpTopicSubscriber<PointCloud2> on this client - centering inactive");
69 return;
70 }
73 {
74 attitudeSub_ = this->getNode()->create_subscription<px4_msgs::msg::VehicleAttitude>(
75 "/fmu/out/vehicle_attitude", rclcpp::SensorDataQoS(),
76 std::bind(&CpTunnelCentering::onAttitude, this, std::placeholders::_1));
77 }
78 lastLog_ = std::chrono::steady_clock::now();
79 RCLCPP_INFO(
80 getLogger(),
81 "CpTunnelCentering: subscribed - slab %.1f..%.1f m ahead, floor clearance %.2f m (min %.1f), "
82 "ceiling clearance %.1f m, max lateral %.1f m, max vertical %.1f m, wall fade %.1f m",
86}
87
89{
90 const int64_t last = lastCloudNs_.load();
91 return last > 0 && (nowNs() - last) < 1000000000LL;
92}
93
94void CpTunnelCentering::onAttitude(const px4_msgs::msg::VehicleAttitude::SharedPtr msg)
95{
96 std::lock_guard<std::mutex> lock(attitudeMutex_);
97 attitudeQ_ = {msg->q[0], msg->q[1], msg->q[2], msg->q[3]};
98 haveAttitude_ = true;
99}
100
101void CpTunnelCentering::onCloud(const sensor_msgs::msg::PointCloud2 & msg)
102{
103 std::array<float, 9> r{};
104 bool level = false;
106 {
107 std::array<float, 4> q;
108 {
109 std::lock_guard<std::mutex> lock(attitudeMutex_);
110 level = haveAttitude_;
111 q = attitudeQ_;
112 }
113 if (level)
114 {
116 }
117 }
118
119 std::vector<float> leftY, rightY, floorZ, ceilZ;
120 leftY.reserve(512);
121 rightY.reserve(512);
122 floorZ.reserve(512);
123 ceilZ.reserve(512);
124
125 const float minRangeSq = params_.minRangeM * params_.minRangeM;
126 sensor_msgs::PointCloud2ConstIterator<float> ix(msg, "x");
127 sensor_msgs::PointCloud2ConstIterator<float> iy(msg, "y");
128 sensor_msgs::PointCloud2ConstIterator<float> iz(msg, "z");
129 for (; ix != ix.end(); ++ix, ++iy, ++iz)
130 {
131 float x = *ix;
132 float y = *iy;
133 float z = *iz;
134 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
135 {
136 continue;
137 }
138 if (x * x + y * y + z * z < minRangeSq)
139 {
140 continue; // own airframe
141 }
142 if (level)
143 {
144 const float lx = r[0] * x + r[1] * y + r[2] * z;
145 const float ly = r[3] * x + r[4] * y + r[5] * z;
146 const float lz = r[6] * x + r[7] * y + r[8] * z;
147 x = lx;
148 y = ly;
149 z = lz;
150 }
151 if (x < -params_.floorLookBackM || x > params_.lookAheadMaxM)
152 {
153 continue;
154 }
155 const bool ahead = x >= params_.lookAheadMinM;
156 // walls: the band at flight height, in the slab ahead
157 if (
158 ahead && std::fabs(z) <= params_.wallBandHalfHeightM &&
159 std::fabs(y) <= params_.lateralSearchM)
160 {
161 if (y > 0.0f)
162 {
163 leftY.push_back(y);
164 }
165 else if (y < 0.0f)
166 {
167 rightY.push_back(-y);
168 }
169 }
170 // floor / ceiling: the column along the heading, EXCLUDING the wall band -
171 // rock beside the vehicle at sensor height is a wall (lateral channel), not
172 // a floor 0.1 m below it (run 22: an arch wall read as "floor 0.1" forced a
173 // 1 m climb into the ceiling)
174 if (
175 std::fabs(y) <= params_.columnHalfWidthM && std::fabs(z) > params_.wallBandHalfHeightM &&
176 std::fabs(z) <= params_.verticalSearchM)
177 {
178 if (z < 0.0f)
179 {
180 floorZ.push_back(-z);
181 }
182 else
183 {
184 ceilZ.push_back(z);
185 }
186 }
187 }
188
189 // nearest surfaces, robust to a few stray returns
190 const size_t kw = static_cast<size_t>(std::max(params_.minWallPoints, 1));
191 const size_t kf = static_cast<size_t>(std::max(params_.minFloorPoints, 1));
192 const float left = kthSmallest(leftY, kw);
193 const float right = kthSmallest(rightY, kw);
194 const float floorD = kthSmallest(floorZ, kf);
195 const float ceilD = kthSmallest(ceilZ, kf);
196
197 // lateral: toward the midpoint between the walls; needs both, fades when far
198 float lateral = 0.0f;
199 if (std::isfinite(left) && std::isfinite(right))
200 {
201 lateral = 0.5f * (left - right); // > 0: more room on the left -> move left
202 const float nearer = std::min(left, right);
203 if (nearer > params_.wallFadeM)
204 {
205 lateral = 0.0f;
206 }
207 else if (nearer > 0.5f * params_.wallFadeM)
208 {
209 lateral *= (params_.wallFadeM - nearer) / (0.5f * params_.wallFadeM);
210 }
211 }
212 lateral = std::clamp(lateral, -params_.maxLateralM, params_.maxLateralM);
213
214 // vertical: hold the floor clearance, but stay below the ceiling
215 float vertical = 0.0f;
216 if (std::isfinite(floorD))
217 {
218 vertical = params_.floorClearanceM - floorD; // > 0: climb
219 }
220 if (std::isfinite(ceilD))
221 {
222 vertical = std::min(vertical, ceilD - params_.ceilingClearanceM);
223 }
224 if (std::isfinite(floorD))
225 {
226 // never below the minimum floor clearance, even when the overhead reading
227 // (possibly rising terrain ahead) asks for it
228 vertical = std::max(vertical, params_.minFloorClearanceM - floorD);
229 }
230 vertical = std::clamp(vertical, -params_.maxVerticalM, params_.maxVerticalM);
231
232 const float a = std::clamp(params_.smoothing, 0.0f, 0.95f);
233 const bool first = lastCloudNs_.load() == 0;
234 lateral_ = first ? lateral : a * lateral_.load() + (1.0f - a) * lateral;
235 vertical_ = first ? vertical : a * vertical_.load() + (1.0f - a) * vertical;
236 left_ = left;
237 right_ = right;
238 floor_ = floorD;
239 ceiling_ = ceilD;
240 lastCloudNs_ = nowNs();
241
242 const auto now = std::chrono::steady_clock::now();
243 if (std::chrono::duration<double>(now - lastLog_).count() >= 2.0)
244 {
245 lastLog_ = now;
246 RCLCPP_INFO(
247 getLogger(),
248 "CpTunnelCentering: walls L %.1f R %.1f, floor %.1f, ceiling %.1f m -> lateral %+.2f, "
249 "vertical %+.2f m",
250 left, right, floorD, ceilD, lateral_.load(), vertical_.load());
251 }
252}
253
254} // namespace cl_px4_mr
std::chrono::steady_clock::time_point lastLog_
std::atomic< int64_t > lastCloudNs_
CpTunnelCentering(TunnelCenteringParams params={})
rclcpp::Subscription< px4_msgs::msg::VehicleAttitude >::SharedPtr attitudeSub_
void onCloud(const sensor_msgs::msg::PointCloud2 &msg)
void onAttitude(const px4_msgs::msg::VehicleAttitude::SharedPtr msg)
smacc2::client_core_components::CpTopicSubscriber< sensor_msgs::msg::PointCloud2 > * subscriber_
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)
float kthSmallest(std::vector< float > &v, size_t k)
void levelRotationFromPx4Quaternion(const std::array< float, 4 > &q, std::array< float, 9 > &r)