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
25
26#include <algorithm>
27#include <cmath>
28
29namespace cl_px4_mr
30{
31
33
35{
36 if (localPosition_ == nullptr || trajectorySetpoint_ == nullptr)
37 {
38 RCLCPP_ERROR(
39 getLogger(), "%s: local position / trajectory setpoint component missing - posting failure",
40 behaviorName());
41 this->postPx4Failure();
42 return;
43 }
44 if (!localPosition_->isValid())
45 {
46 RCLCPP_ERROR(
47 getLogger(), "%s: no valid local position at entry - posting failure", behaviorName());
48 this->postPx4Failure();
49 return;
50 }
51
52 NedPoint current;
53 current.x = localPosition_->getX();
54 current.y = localPosition_->getY();
55 current.z = localPosition_->getZ();
56 current.yaw = localPosition_->getHeading();
57 entryHeading_ = current.yaw;
58
59 std::vector<NedPoint> raw = buildPath(current);
60 if (raw.empty())
61 {
62 RCLCPP_ERROR(
63 getLogger(), "%s: buildPath() returned no vertices - posting failure", behaviorName());
64 this->postPx4Failure();
65 return;
66 }
67
68 path_.clear();
70 {
71 NedPoint start = current;
72 start.yaw = std::numeric_limits<float>::quiet_NaN();
73 path_.push_back(start);
74 }
75 for (const NedPoint & v : raw)
76 {
77 if (!path_.empty() && nedDistance(path_.back(), v) < followerParams_.minSegmentLength)
78 {
79 // keep the later vertex's yaw, drop the degenerate segment
80 path_.back().yaw = std::isnan(v.yaw) ? path_.back().yaw : v.yaw;
81 continue;
82 }
83 path_.push_back(v);
84 }
85
87 totalLen_ = cumLen_.empty() ? 0.0f : cumLen_.back();
88 sCarrot_ = 0.0f;
90
91 const float speed = std::max(followerParams_.groundSpeed, 0.05f);
92 const float lag = speed / 0.95f;
93 if (followerParams_.leash < 1.2f * lag)
94 {
95 RCLCPP_WARN(
96 getLogger(),
97 "%s: leash %.1f m is below 1.2x the expected tracking lag (%.1f m at %.1f m/s) - effective "
98 "speed will be throttled to ~%.1f m/s",
100 }
101
102 if (!this->hasTimeout() && followerParams_.autoTimeoutFactor > 0.0f)
103 {
104 const float seconds = std::max(30.0f, followerParams_.autoTimeoutFactor * totalLen_ / speed);
105 this->setTimeout(std::chrono::milliseconds(static_cast<int64_t>(seconds * 1000.0f)));
106 RCLCPP_INFO(getLogger(), "%s: auto-timeout armed at %.0f s", behaviorName(), seconds);
107 }
108
109 RCLCPP_INFO(
110 getLogger(),
111 "%s: following %zu vertices, %.1f m at %.1f m/s (leash %.1f m) from NED (%.1f, %.1f, %.1f) "
112 "to (%.1f, %.1f, %.1f)",
113 behaviorName(), path_.size(), totalLen_, speed, followerParams_.leash, path_.front().x,
114 path_.front().y, path_.front().z, path_.back().x, path_.back().y, path_.back().z);
116
117 lastCmd_ = commandFor(0.0f);
119 lastUpdateTime_ = std::chrono::steady_clock::now();
120 active_ = true;
121}
122
124{
125 if (active_.exchange(false))
126 {
127 // hold at the last carrot (not hold(): that would step the setpoint back
128 // to the lagging vehicle position)
130 RCLCPP_INFO(
131 getLogger(), "%s: exiting at %.0f%% - holding at last setpoint (%.1f, %.1f, %.1f)",
133 }
134}
135
137{
139
140 if (!active_)
141 {
142 return;
143 }
144
145 const auto now = std::chrono::steady_clock::now();
146 float dt = std::chrono::duration<float>(now - lastUpdateTime_).count();
147 lastUpdateTime_ = now;
148 dt = std::clamp(dt, 0.0f, 0.5f);
149
150 if (!localPosition_->isValid())
151 {
152 // freeze the carrot; the watchdog covers a lost position
153 return;
154 }
155
156 NedPoint vehicle;
157 vehicle.x = localPosition_->getX();
158 vehicle.y = localPosition_->getY();
159 vehicle.z = localPosition_->getZ();
160
161 // advance the carrot, throttled by the leash
162 const float sNext = std::min(sCarrot_ + followerParams_.groundSpeed * dt, totalLen_);
163 const NedPoint candidate = sampleAtArcLength(path_, cumLen_, sNext);
164 if (nedDistance(candidate, vehicle) <= followerParams_.leash)
165 {
166 sCarrot_ = sNext;
167 }
168
171
172 const int decile = static_cast<int>(progressFraction() * 10.0f);
173 if (decile != lastProgressDecile_ && decile > 0 && decile < 10)
174 {
175 lastProgressDecile_ = decile;
176 RCLCPP_INFO(
177 getLogger(), "%s: %d%% (%.0f / %.0f m)", behaviorName(), decile * 10, sCarrot_, totalLen_);
178 }
179
180 // completion: carrot at the end and vehicle within tolerance of the last vertex
181 if (sCarrot_ >= totalLen_)
182 {
183 const NedPoint & end = path_.back();
184 const float dxy = nedDistanceXY(vehicle, end);
185 const float dz = std::fabs(vehicle.z - end.z);
187 {
188 active_ = false;
189 RCLCPP_INFO(
190 getLogger(), "%s: path complete (xy err %.2f m, z err %.2f m) - posting success",
191 behaviorName(), dxy, dz);
193 this->postPx4Success();
194 }
195 }
196}
197
198float CbPx4PathFollowerBase::tangentYawAt(size_t segmentIndex) const
199{
200 if (path_.size() < 2)
201 {
202 return entryHeading_;
203 }
204 const size_t i0 = std::min(segmentIndex, path_.size() - 2);
205 const NedPoint & a = path_[i0];
206 const NedPoint & b = path_[i0 + 1];
207 const float dx = b.x - a.x;
208 const float dy = b.y - a.y;
209 if (std::hypot(dx, dy) < 1e-3f)
210 {
211 return entryHeading_; // vertical segment: keep heading
212 }
213 return std::atan2(dy, dx);
214}
215
217{
218 size_t segment = 0;
219 NedPoint cmd = sampleAtArcLength(path_, cumLen_, s, &segment);
220
221 switch (followerParams_.yawMode)
222 {
223 case YawMode::FIXED:
225 break;
227 cmd.yaw = entryHeading_;
228 break;
230 if (std::isnan(cmd.yaw))
231 {
232 cmd.yaw = tangentYawAt(segment);
233 }
234 break;
235 case YawMode::TANGENT:
236 default:
237 cmd.yaw = tangentYawAt(segment);
238 break;
239 }
240 cmd.yaw = wrapPi(cmd.yaw);
241 return cmd;
242}
243
244} // 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
virtual const char * behaviorName() 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)