SMACC2
Loading...
Searching...
No Matches
cb_px4_path_follower_base.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
26
27#include <algorithm>
28#include <cmath>
29
30namespace cl_px4_mr
31{
32
34
36{
37 if (localPosition_ == nullptr || trajectorySetpoint_ == nullptr)
38 {
39 RCLCPP_ERROR(
40 getLogger(), "%s: local position / trajectory setpoint component missing - posting failure",
41 behaviorName());
42 this->postPx4Failure();
43 return;
44 }
45 if (!localPosition_->isValid())
46 {
47 RCLCPP_ERROR(
48 getLogger(), "%s: no valid local position at entry - posting failure", behaviorName());
49 this->postPx4Failure();
50 return;
51 }
52
53 NedPoint current;
54 current.x = localPosition_->getX();
55 current.y = localPosition_->getY();
56 current.z = localPosition_->getZ();
57 current.yaw = localPosition_->getHeading();
58 entryHeading_ = current.yaw;
59
60 std::vector<NedPoint> raw = buildPath(current);
61 if (raw.empty())
62 {
63 RCLCPP_ERROR(
64 getLogger(), "%s: buildPath() returned no vertices - posting failure", behaviorName());
65 this->postPx4Failure();
66 return;
67 }
68
69 path_.clear();
71 {
72 NedPoint start = current;
73 start.yaw = std::numeric_limits<float>::quiet_NaN();
74 path_.push_back(start);
75 }
76 for (const NedPoint & v : raw)
77 {
78 if (!path_.empty() && nedDistance(path_.back(), v) < followerParams_.minSegmentLength)
79 {
80 // keep the later vertex's yaw, drop the degenerate segment
81 path_.back().yaw = std::isnan(v.yaw) ? path_.back().yaw : v.yaw;
82 continue;
83 }
84 path_.push_back(v);
85 }
86
88 totalLen_ = cumLen_.empty() ? 0.0f : cumLen_.back();
89 sCarrot_ = 0.0f;
90 carrotVertex_ = 0;
92
93 const float speed = std::max(followerParams_.groundSpeed, 0.05f);
94 const float lag = speed / 0.95f;
95 if (followerParams_.leash < 1.2f * lag)
96 {
97 RCLCPP_WARN(
98 getLogger(),
99 "%s: leash %.1f m is below 1.2x the expected tracking lag (%.1f m at %.1f m/s) - effective "
100 "speed will be throttled to ~%.1f m/s",
101 behaviorName(), followerParams_.leash, lag, speed, 0.95f * followerParams_.leash);
102 }
103
104 if (!this->hasTimeout() && followerParams_.autoTimeoutFactor > 0.0f)
105 {
106 const float seconds = std::max(30.0f, followerParams_.autoTimeoutFactor * totalLen_ / speed);
107 this->setTimeout(std::chrono::milliseconds(static_cast<int64_t>(seconds * 1000.0f)));
108 RCLCPP_INFO(getLogger(), "%s: auto-timeout armed at %.0f s", behaviorName(), seconds);
109 }
110
111 RCLCPP_INFO(
112 getLogger(),
113 "%s: following %zu vertices, %.1f m at %.1f m/s (leash %.1f m) from NED (%.1f, %.1f, %.1f) "
114 "to (%.1f, %.1f, %.1f)%s",
115 behaviorName(), path_.size(), totalLen_, speed, followerParams_.leash, path_.front().x,
116 path_.front().y, path_.front().z, path_.back().x, path_.back().y, path_.back().z,
118 ? (tunnelCentering_ ? " + tunnel centering"
119 : " (tunnel centering requested but no component)")
120 : "");
122
123 lastCmd_ = commandFor(0.0f);
125 lastUpdateTime_ = std::chrono::steady_clock::now();
126 active_ = true;
127}
128
130{
131 if (active_.exchange(false))
132 {
133 // hold at the last carrot (not hold(): that would step the setpoint back
134 // to the lagging vehicle position)
136 RCLCPP_INFO(
137 getLogger(), "%s: exiting at %.0f%% - holding at last setpoint (%.1f, %.1f, %.1f)",
139 }
140}
141
143{
145
146 if (!active_)
147 {
148 return;
149 }
150
151 const auto now = std::chrono::steady_clock::now();
152 float dt = std::chrono::duration<float>(now - lastUpdateTime_).count();
153 lastUpdateTime_ = now;
154 dt = std::clamp(dt, 0.0f, 0.5f);
155
156 if (!localPosition_->isValid())
157 {
158 // freeze the carrot; the watchdog covers a lost position
159 return;
160 }
161
162 NedPoint vehicle;
163 vehicle.x = localPosition_->getX();
164 vehicle.y = localPosition_->getY();
165 vehicle.z = localPosition_->getZ();
166
167 // advance the carrot, throttled by the leash (measured to the carrot as it
168 // will actually be commanded, centering offsets included)
169 const float sNext = std::min(sCarrot_ + followerParams_.groundSpeed * dt, totalLen_);
170 const NedPoint candidate = commandFor(sNext);
171 if (nedDistance(candidate, vehicle) <= followerParams_.leash)
172 {
173 sCarrot_ = sNext;
174 }
175
178
179 // vertex most recently passed by the carrot (cumLen_[0] == 0, so the
180 // upper_bound is never begin())
181 {
182 const size_t ub = std::upper_bound(cumLen_.begin(), cumLen_.end(), sCarrot_) - cumLen_.begin();
183 const size_t idx = ub == 0 ? 0 : ub - 1;
184 carrotVertex_ = path_.empty() ? 0 : std::min(idx, path_.size() - 1);
185 }
186
187 const int decile = static_cast<int>(progressFraction() * 10.0f);
188 if (decile != lastProgressDecile_ && decile > 0 && decile < 10)
189 {
190 lastProgressDecile_ = decile;
191 RCLCPP_INFO(
192 getLogger(), "%s: %d%% (%.0f / %.0f m)", behaviorName(), decile * 10, sCarrot_, totalLen_);
193 }
194
195 // completion: carrot at the end and vehicle within tolerance of the final
196 // commanded carrot (== the last vertex unless centering offsets it)
197 if (sCarrot_ >= totalLen_)
198 {
199 const NedPoint & end = lastCmd_;
200 const float dxy = nedDistanceXY(vehicle, end);
201 const float dz = std::fabs(vehicle.z - end.z);
203 {
204 active_ = false;
205 RCLCPP_INFO(
206 getLogger(), "%s: path complete (xy err %.2f m, z err %.2f m) - posting success",
207 behaviorName(), dxy, dz);
209 this->postPx4Success();
210 }
211 }
212}
213
214float CbPx4PathFollowerBase::tangentYawAt(size_t segmentIndex) const
215{
216 if (path_.size() < 2)
217 {
218 return entryHeading_;
219 }
220 const size_t i0 = std::min(segmentIndex, path_.size() - 2);
221 const NedPoint & a = path_[i0];
222 const NedPoint & b = path_[i0 + 1];
223 const float dx = b.x - a.x;
224 const float dy = b.y - a.y;
225 if (std::hypot(dx, dy) < 1e-3f)
226 {
227 return entryHeading_; // vertical segment: keep heading
228 }
229 return std::atan2(dy, dx);
230}
231
233{
234 size_t segment = 0;
235 NedPoint cmd = sampleAtArcLength(path_, cumLen_, s, &segment);
236
237 switch (followerParams_.yawMode)
238 {
239 case YawMode::FIXED:
241 break;
243 cmd.yaw = entryHeading_;
244 break;
246 if (std::isnan(cmd.yaw))
247 {
248 cmd.yaw = tangentYawAt(segment);
249 }
250 break;
251 case YawMode::TANGENT:
252 default:
253 cmd.yaw = tangentYawAt(segment);
254 break;
255 }
256 cmd.yaw = wrapPi(cmd.yaw);
257 applyCentering(cmd);
258 return cmd;
259}
260
262{
263 if (
265 !tunnelCentering_->valid() || localPosition_ == nullptr)
266 {
267 return;
268 }
269 const float up = tunnelCentering_->verticalOffsetM(); // > 0: climb
270 cmd.z -= up;
271 // the lateral offset is measured in the vehicle's own heading frame (the
272 // lidar's); apply it only while that heading agrees with the path tangent -
273 // mid-turn "left" can point straight at the wall the path leads away from
274 const float heading = localPosition_->getHeading();
275 const float misalign = wrapPi(heading - cmd.yaw);
276 if (std::fabs(misalign) > 0.7854f)
277 {
278 return;
279 }
280 const float lateral = tunnelCentering_->lateralOffsetM(); // > 0: left of the vehicle
281 // NED: heading psi from north toward east; left of forward = (sin psi, -cos psi)
282 const float s = std::sin(heading);
283 const float c = std::cos(heading);
284 cmd.x += lateral * s;
285 cmd.y -= lateral * c;
286}
287
288} // namespace cl_px4_mr
void setTimeout(std::chrono::milliseconds timeout)
std::chrono::steady_clock::time_point lastUpdateTime_
CbPx4PathFollowerBase(PathFollowerParams params={})
virtual std::vector< NedPoint > buildPath(const NedPoint &current)=0
virtual void onPathStarted(const std::vector< NedPoint > &)
float tangentYawAt(size_t segmentIndex) const
void setPositionNED(float x, float y, float z, float yaw=std::numeric_limits< float >::quiet_NaN())
virtual rclcpp::Logger getLogger() const
NedPoint sampleAtArcLength(const std::vector< NedPoint > &path, const std::vector< float > &cum, float s, size_t *segmentIndex=nullptr)
float nedDistance(const NedPoint &a, const NedPoint &b)
float nedDistanceXY(const NedPoint &a, const NedPoint &b)
float wrapPi(float angle)
std::vector< float > cumulativeLengths(const std::vector< NedPoint > &path)