Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ardupilot
GitHub Repository: Ardupilot/ardupilot
Path: blob/master/libraries/AC_WPNav/AC_Circle.cpp
9534 views
1
#include <AP_HAL/AP_HAL.h>
2
#include <AP_Math/AP_Math.h>
3
#include <AP_Terrain/AP_Terrain.h>
4
#include "AC_Circle.h"
5
#include <AP_Logger/AP_Logger.h>
6
#include <AP_Vehicle/AP_Vehicle_Type.h>
7
8
extern const AP_HAL::HAL& hal;
9
10
// Circle mode default and limit constants
11
#define AC_CIRCLE_RADIUS_M_DEFAULT 10.0f // Default circle radius in meters
12
#define AC_CIRCLE_RATE_DEFAULT 20.0f // Default circle turn rate in degrees per second. Positive = clockwise, negative = counter-clockwise.
13
#define AC_CIRCLE_ANGULAR_ACCEL_MIN 2.0f // Minimum angular acceleration in deg/s² (used to avoid sluggish yaw transitions).
14
#define AC_CIRCLE_RADIUS_MAX_M 2000.0 // Maximum allowed circle radius in meters (2000 m = 2 km).
15
16
const AP_Param::GroupInfo AC_Circle::var_info[] = {
17
// 0 was RADIUS (cm)
18
19
// @Param: RATE
20
// @DisplayName: Circle rate
21
// @Description: Circle mode's maximum turn rate in deg/sec. Positive to turn clockwise, negative for counter clockwise. Final circle rate is also limited by speed and acceleration settings.
22
// @Units: deg/s
23
// @Range: -90 90
24
// @Increment: 1
25
// @User: Standard
26
AP_GROUPINFO("RATE", 1, AC_Circle, _rate_parm_degs, AC_CIRCLE_RATE_DEFAULT),
27
28
// @Param: OPTIONS
29
// @DisplayName: Circle options
30
// @Description: 0:Enable or disable using the pitch/roll stick control circle mode's radius and rate
31
// @Bitmask: 0:manual control, 1:face direction of travel, 2:Start at center rather than on perimeter, 3:Make Mount ROI the center of the circle
32
// @User: Standard
33
AP_GROUPINFO("OPTIONS", 2, AC_Circle, _options, 1),
34
35
// @Param: RADIUS_M
36
// @DisplayName: Circle Radius
37
// @Description: Radius of the circle the vehicle will fly when in Circle flight mode
38
// @Units: m
39
// @Range: 0 2000
40
// @Increment: 1
41
// @User: Standard
42
AP_GROUPINFO("RADIUS_M", 3, AC_Circle, _radius_parm_m, AC_CIRCLE_RADIUS_M_DEFAULT),
43
44
AP_GROUPEND
45
};
46
47
// Default constructor.
48
// Note that the Vector/Matrix constructors already implicitly zero
49
// their values.
50
AC_Circle::AC_Circle(const AP_AHRS_View& ahrs, AC_PosControl& pos_control) :
51
_ahrs(ahrs),
52
_pos_control(pos_control)
53
{
54
AP_Param::setup_object_defaults(this, var_info);
55
56
_rotation_rate_max_rads = radians(_rate_parm_degs);
57
}
58
59
// Initializes circle flight using a center position in centimeters relative to the EKF origin.
60
// See init_NED_m() for full details.
61
void AC_Circle::init_NEU_cm(const Vector3p& center_neu_cm, bool is_terrain_alt, float rate_degs)
62
{
63
// Convert input from NEU cm to NED meters and delegate to meter-based initializer
64
Vector3p center_ned_m = Vector3p{center_neu_cm.x, center_neu_cm.y, -center_neu_cm.z} * 0.01;
65
init_NED_m(center_ned_m, is_terrain_alt, rate_degs);
66
}
67
68
// Initializes circle flight mode using a specified center position in meters.
69
// Parameters:
70
// - center_ned_m: Center of the circle in NED frame (meters, relative to EKF origin)
71
// - is_terrain_alt: If true, center_ned_m.z is interpreted as relative to terrain; otherwise, above EKF origin
72
// - rate_degs: Desired turn rate in degrees per second (positive = clockwise, negative = counter-clockwise)
73
// Caller must preconfigure the position controller's speed and acceleration settings before calling.
74
void AC_Circle::init_NED_m(const Vector3p& center_ned_m, bool is_terrain_alt, float rate_degs)
75
{
76
// Store circle center and frame reference
77
_center_ned_m = center_ned_m;
78
_is_terrain_alt = is_terrain_alt;
79
80
// Convert desired turn rate from degrees to radians
81
_rotation_rate_max_rads = radians(rate_degs);
82
83
// Initialise position controller using current lean angles
84
_pos_control.NE_init_controller_stopping_point();
85
_pos_control.D_init_controller_stopping_point();
86
87
// Calculate velocity and acceleration limits based on circle configuration
88
calc_velocities(true);
89
90
// Use position-based angle initialization (avoids sharp yaw discontinuity)
91
init_start_angle(false);
92
}
93
94
// Initializes the circle flight mode using the current stopping point as a reference.
95
// If the INIT_AT_CENTER option is not set, the circle center is projected one radius ahead along the vehicle's heading.
96
// Caller must configure position controller speeds and accelerations beforehand.
97
void AC_Circle::init()
98
{
99
// Load radius and rate from parameters
100
_radius_m = _radius_parm_m.get();
101
_last_radius_param_m = _radius_m;
102
_rotation_rate_max_rads = radians(_rate_parm_degs);
103
104
// Initialise position controller using current lean angles
105
_pos_control.NE_init_controller_stopping_point();
106
_pos_control.D_init_controller_stopping_point();
107
108
// Get stopping point as initial reference for center
109
const Vector3p& stopping_point_ned_m = _pos_control.get_pos_desired_NED_m();
110
111
// By default, set center to stopping point
112
_center_ned_m = stopping_point_ned_m;
113
114
// If INIT_AT_CENTER is not set, project center forward by radius in heading direction
115
if ((_options.get() & CircleOptions::INIT_AT_CENTER) == 0) {
116
_center_ned_m.x += _radius_m * _ahrs.cos_yaw();
117
_center_ned_m.y += _radius_m * _ahrs.sin_yaw();
118
}
119
120
// Circle altitude is relative to EKF origin by default
121
_is_terrain_alt = false;
122
123
// Calculate velocity and acceleration constraints
124
calc_velocities(true);
125
126
// Initialize angle using vehicle heading
127
init_start_angle(true);
128
}
129
130
// Sets the circle center using a Location object.
131
// Automatically determines whether the location uses terrain-relative or origin-relative altitude.
132
// If conversion fails, defaults to current position and logs a navigation error.
133
void AC_Circle::set_center(const Location& center)
134
{
135
// Check if the location uses terrain-relative altitude
136
if (center.get_alt_frame() == Location::AltFrame::ABOVE_TERRAIN) {
137
// convert Location with terrain altitude
138
Vector2p center_ne_m;
139
float terr_alt_m;
140
141
// Attempt to convert XY and Z to NEU frame with terrain altitude
142
if (center.get_vector_xy_from_origin_NE_m(center_ne_m) && center.get_alt_m(Location::AltFrame::ABOVE_TERRAIN, terr_alt_m)) {
143
set_center_NED_m(Vector3p{center_ne_m.x, center_ne_m.y, -terr_alt_m}, true);
144
} else {
145
// Conversion failed: fall back to current position and log error
146
set_center_NED_m(_pos_control.get_pos_estimate_NED_m(), false);
147
LOGGER_WRITE_ERROR(LogErrorSubsystem::NAVIGATION, LogErrorCode::FAILED_CIRCLE_INIT);
148
}
149
} else {
150
// Handle alt-above-origin, alt-above-home, or absolute altitudes
151
Vector3p circle_center_ned_m;
152
if (!center.get_vector_from_origin_NED_m(circle_center_ned_m)) {
153
// Conversion failed: fall back to current position and log error
154
circle_center_ned_m = _pos_control.get_pos_estimate_NED_m();
155
LOGGER_WRITE_ERROR(LogErrorSubsystem::NAVIGATION, LogErrorCode::FAILED_CIRCLE_INIT);
156
}
157
158
// Apply converted center and mark it as origin-relative
159
set_center_NED_m(circle_center_ned_m, false);
160
}
161
}
162
163
// Sets the target circle rate in degrees per second.
164
// Positive values result in clockwise rotation; negative for counter-clockwise.
165
void AC_Circle::set_rate_degs(float rate_degs)
166
{
167
// Convert the rate from degrees/sec to radians/sec
168
_rotation_rate_max_rads = radians(rate_degs);
169
}
170
171
// Sets the circle radius in centimeters.
172
// See set_radius_m() for full details.
173
void AC_Circle::set_radius_cm(float radius_cm)
174
{
175
// Convert input from cm to meters and call set_radius_m()
176
set_radius_m(radius_cm * 0.01);
177
}
178
179
// Sets the circle radius in meters.
180
// Radius is constrained to AC_CIRCLE_RADIUS_MAX_M.
181
void AC_Circle::set_radius_m(float radius_m)
182
{
183
// Constrain the radius to prevent unsafe or invalid circle sizes
184
_radius_m = constrain_float(radius_m, 0, AC_CIRCLE_RADIUS_MAX_M);
185
}
186
187
// Returns true if the circle controller's update() function has run recently.
188
// Used by vehicle code to determine if yaw and position outputs are valid.
189
bool AC_Circle::is_active() const
190
{
191
// Consider the controller active if the last update occurred within the past 200 milliseconds
192
return (AP_HAL::millis() - _last_update_ms < 200);
193
}
194
195
// Updates the circle controller using a climb rate in cm/s.
196
// See update_ms() for full implementation details.
197
bool AC_Circle::update_cms(float climb_rate_cms)
198
{
199
// Convert climb rate from cm/s to m/s and call the meter-based update
200
return update_ms(climb_rate_cms * 0.01);
201
}
202
203
// Updates the circle controller using a climb rate in m/s.
204
// Computes new angular position, yaw, and vertical trajectory, then updates the position controller.
205
// Returns false if terrain data is required but unavailable.
206
bool AC_Circle::update_ms(float climb_rate_ms)
207
{
208
// Recalculate angular velocities based on the current radius and rate
209
calc_velocities(false);
210
211
// calculate dt
212
const float dt = _pos_control.get_dt_s();
213
214
// ramp angular velocity to maximum
215
if (_angular_vel_rads < _angular_vel_max_rads) {
216
_angular_vel_rads += fabsf(_angular_accel_radss) * dt;
217
_angular_vel_rads = MIN(_angular_vel_rads, _angular_vel_max_rads);
218
}
219
if (_angular_vel_rads > _angular_vel_max_rads) {
220
_angular_vel_rads -= fabsf(_angular_accel_radss) * dt;
221
_angular_vel_rads = MAX(_angular_vel_rads, _angular_vel_max_rads);
222
}
223
224
// update the target angle and total angle travelled
225
float angle_change_rad = _angular_vel_rads * dt;
226
_angle_rad += angle_change_rad;
227
_angle_rad = wrap_PI(_angle_rad);
228
_angle_total_rad += angle_change_rad;
229
230
// calculate terrain adjustments
231
float terrain_u_m = 0.0f;
232
if (_is_terrain_alt && !get_terrain_U_m(terrain_u_m)) {
233
return false;
234
}
235
236
// calculate z-axis target
237
float target_d_m;
238
if (_is_terrain_alt) {
239
target_d_m = _center_ned_m.z - terrain_u_m;
240
} else {
241
target_d_m = -_pos_control.get_pos_desired_U_m();
242
}
243
244
// Construct target position centered on the circle center
245
Vector3p target_ned_m {
246
_center_ned_m.x,
247
_center_ned_m.y,
248
target_d_m
249
};
250
if (!is_zero(_radius_m)) {
251
// Calculate position on the circle edge based on current angle
252
target_ned_m.x += _radius_m * cosf(-_angle_rad);
253
target_ned_m.y += - _radius_m * sinf(-_angle_rad);
254
255
// Compute yaw toward the circle center
256
_yaw_rad = get_bearing_rad(_pos_control.get_pos_desired_NED_m().xy().tofloat(), _center_ned_m.xy().tofloat());
257
258
// Optionally adjust yaw to face direction of travel
259
if ((_options.get() & CircleOptions::FACE_DIRECTION_OF_TRAVEL) != 0) {
260
_yaw_rad += is_positive(_rotation_rate_max_rads) ? -radians(90.0) : radians(90.0);
261
_yaw_rad = wrap_2PI(_yaw_rad);
262
}
263
} else {
264
// set heading be the same as the angle for zero radius
265
_yaw_rad = _angle_rad;
266
}
267
268
// update position controller target
269
Vector2f zero_ne;
270
_pos_control.input_pos_vel_accel_NE_m(target_ned_m.xy(), zero_ne, zero_ne);
271
if (_is_terrain_alt) {
272
float vel_zero = 0;
273
float target_pos_d_m = target_ned_m.z;
274
_pos_control.input_pos_vel_accel_D_m(target_pos_d_m, vel_zero, 0);
275
} else {
276
_pos_control.D_set_pos_target_from_climb_rate_ms(climb_rate_ms);
277
}
278
279
// update position controller
280
_pos_control.NE_update_controller();
281
282
// set update time
283
_last_update_ms = AP_HAL::millis();
284
285
return true;
286
}
287
288
// Returns the closest point on the circle to the vehicle's current position in centimeters.
289
// See get_closest_point_on_circle_NED_m() for full details.
290
void AC_Circle::get_closest_point_on_circle_NEU_cm(Vector3f& result_neu_cm, float& dist_cm) const
291
{
292
// Convert input arguments from neu cm to ned meters
293
Vector3p result_ned_m = Vector3p{result_neu_cm.x, result_neu_cm.y, -result_neu_cm.z} * 0.01;
294
float dist_m = dist_cm * 0.01;
295
296
// Compute closest point in meters
297
get_closest_point_on_circle_NED_m(result_ned_m, dist_m);
298
299
// Convert results back to neu centimeters
300
result_neu_cm = Vector3f(result_ned_m.x, result_ned_m.y, -result_ned_m.z) * 100.0;
301
dist_cm = dist_m * 100.0;
302
}
303
304
// Returns the closest point on the circle to the vehicle's stopping point in meters.
305
// The result vector is updated with the NEU position of the closest point on the circle.
306
// The altitude (z) is set to match the circle center's altitude.
307
// dist_m is updated with the 3D distance to the circle edge from the stopping point.
308
// If the vehicle is at the center, the point directly behind the vehicle (based on yaw) is returned.
309
void AC_Circle::get_closest_point_on_circle_NED_m(Vector3p& result_ned_m, float& dist_to_edge_m) const
310
{
311
// Get vehicle stopping point from the position controller (in NEU frame, meters)
312
Vector3p stopping_point_ned_m;
313
_pos_control.get_stopping_point_NE_m(stopping_point_ned_m.xy());
314
_pos_control.get_stopping_point_D_m(stopping_point_ned_m.z);
315
316
// Compute vector from stopping point to the circle center
317
Vector3f vec_from_center_ned_m = (stopping_point_ned_m - _center_ned_m).tofloat();
318
// Return circle center if radius is zero (vehicle orbits in place)
319
if (!is_positive(_radius_m)) {
320
result_ned_m = _center_ned_m;
321
dist_to_edge_m = 0;
322
return;
323
}
324
325
const float dist_to_center_m_sq = vec_from_center_ned_m.length_squared();
326
// Handle edge case: vehicle is at the exact center of the circle
327
if (dist_to_center_m_sq < sq(0.5)) {
328
result_ned_m.x = _center_ned_m.x - _radius_m * _ahrs.cos_yaw();
329
result_ned_m.y = _center_ned_m.y - _radius_m * _ahrs.sin_yaw();
330
result_ned_m.z = _center_ned_m.z;
331
dist_to_edge_m = (stopping_point_ned_m - result_ned_m).length();
332
return;
333
}
334
335
// Calculate the closest point on the circle's edge by projecting out from center
336
const float dist_to_center_m_xy = vec_from_center_ned_m.xy().length();
337
result_ned_m.x = _center_ned_m.x + vec_from_center_ned_m.x / dist_to_center_m_xy * _radius_m;
338
result_ned_m.y = _center_ned_m.y + vec_from_center_ned_m.y / dist_to_center_m_xy * _radius_m;
339
result_ned_m.z = _center_ned_m.z;
340
dist_to_edge_m = (stopping_point_ned_m - result_ned_m).length();
341
}
342
343
// Calculates angular velocity and acceleration limits based on the configured radius and rate.
344
// Should be called whenever radius or rate changes.
345
// If `init_velocity` is true, resets angular velocity to zero (used on controller startup).
346
void AC_Circle::calc_velocities(bool init_velocity)
347
{
348
// If performing a panorama (radius is zero), use current yaw rate and enforce minimum acceleration
349
if (_radius_m <= 0) {
350
_angular_vel_max_rads = _rotation_rate_max_rads;
351
_angular_accel_radss = MAX(fabsf(_angular_vel_max_rads), radians(AC_CIRCLE_ANGULAR_ACCEL_MIN)); // reach maximum yaw velocity in 1 second
352
}else{
353
// Limit max horizontal speed based on radius and available acceleration
354
float vel_max_ms = MIN(_pos_control.NE_get_max_speed_ms(), safe_sqrt(0.5f*_pos_control.NE_get_max_accel_mss()*_radius_m));
355
356
// Convert linear speed to angular velocity (rad/s)
357
_angular_vel_max_rads = vel_max_ms/_radius_m;
358
359
// Constrain to requested circle rate
360
_angular_vel_max_rads = constrain_float(_rotation_rate_max_rads, -_angular_vel_max_rads, _angular_vel_max_rads);
361
362
// Derive maximum angular acceleration
363
_angular_accel_radss = MAX(_pos_control.NE_get_max_accel_mss() / _radius_m, radians(AC_CIRCLE_ANGULAR_ACCEL_MIN));
364
}
365
366
// Reset angular velocity to zero at initialization if requested
367
if (init_velocity) {
368
_angular_vel_rads = 0.0;
369
}
370
}
371
372
// Sets the initial angle around the circle and resets the accumulated angle.
373
// If `use_heading` is true, uses vehicle heading to initialize angle for minimal yaw motion.
374
// If false, uses position relative to circle center to set angle.
375
void AC_Circle::init_start_angle(bool use_heading)
376
{
377
// Reset the accumulated angle traveled around the circle
378
_angle_total_rad = 0.0;
379
380
// If radius is zero, we're doing a panorama—set angle to current yaw heading
381
if (_radius_m <= 0) {
382
_angle_rad = _ahrs.yaw;
383
return;
384
}
385
386
// if use_heading is true
387
if (use_heading) {
388
// Initialize angle directly behind current heading to minimize yaw motion
389
_angle_rad = wrap_PI(_ahrs.yaw - M_PI);
390
} else {
391
// If vehicle is exactly at the center, init angle behind vehicle (prevent undefined bearing)
392
const Vector3p &curr_pos_desired_ned_m = _pos_control.get_pos_desired_NED_m();
393
if (is_equal(curr_pos_desired_ned_m.x, _center_ned_m.x) && is_equal(curr_pos_desired_ned_m.y, _center_ned_m.y)) {
394
_angle_rad = wrap_PI(_ahrs.yaw - M_PI);
395
} else {
396
// Calculate bearing from circle center to current position
397
float bearing_rad = atan2f(curr_pos_desired_ned_m.y - _center_ned_m.y, curr_pos_desired_ned_m.x - _center_ned_m.x);
398
_angle_rad = wrap_PI(bearing_rad);
399
}
400
}
401
}
402
403
// Returns the expected source of terrain data for the circle controller.
404
// Used to determine whether terrain offset comes from rangefinder, terrain database, or is unavailable.
405
AC_Circle::TerrainSource AC_Circle::get_terrain_source() const
406
{
407
// Use rangefinder if available and enabled
408
if (_rangefinder_available) {
409
return AC_Circle::TerrainSource::TERRAIN_FROM_RANGEFINDER;
410
}
411
#if AP_TERRAIN_AVAILABLE
412
// Fallback to terrain database if available and enabled
413
const AP_Terrain *terrain = AP_Terrain::get_singleton();
414
if ((terrain != nullptr) && terrain->enabled()) {
415
return AC_Circle::TerrainSource::TERRAIN_FROM_TERRAINDATABASE;
416
} else {
417
return AC_Circle::TerrainSource::TERRAIN_UNAVAILABLE;
418
}
419
#else
420
// Terrain unavailable if no valid source
421
return AC_Circle::TerrainSource::TERRAIN_UNAVAILABLE;
422
#endif
423
}
424
425
// Returns terrain offset in meters above the EKF origin at the current position.
426
// Positive values indicate terrain is above the EKF origin altitude.
427
// Terrain source may be rangefinder or terrain database.
428
bool AC_Circle::get_terrain_U_m(float& terrain_u_m)
429
{
430
// Determine terrain source and calculate offset accordingly
431
switch (get_terrain_source()) {
432
case AC_Circle::TerrainSource::TERRAIN_UNAVAILABLE:
433
return false;
434
case AC_Circle::TerrainSource::TERRAIN_FROM_RANGEFINDER:
435
if (_rangefinder_healthy) {
436
// Use last known healthy rangefinder terrain offset
437
terrain_u_m = _rangefinder_terrain_u_m;
438
return true;
439
}
440
return false;
441
case AC_Circle::TerrainSource::TERRAIN_FROM_TERRAINDATABASE:
442
#if AP_TERRAIN_AVAILABLE
443
float terr_alt_m = 0.0f;
444
AP_Terrain *terrain = AP_Terrain::get_singleton();
445
if (terrain != nullptr && terrain->height_above_terrain(terr_alt_m, true)) {
446
// Calculate offset from EKF origin altitude to terrain altitude
447
terrain_u_m = _pos_control.get_pos_estimate_U_m() - terr_alt_m;
448
return true;
449
}
450
#endif
451
return false;
452
}
453
454
// This fallback should never be reached
455
return false;
456
}
457
458
// Checks if the circle radius parameter has changed.
459
// If so, updates internal `_radius_m` and stores the new parameter value.
460
void AC_Circle::check_param_change()
461
{
462
// Check if the stored radius param has changed
463
if (!is_equal(_last_radius_param_m, _radius_parm_m.get())) {
464
// update internal radius and store new parameter value to detect future changes
465
_radius_m = _radius_parm_m.get();
466
_last_radius_param_m = _radius_m;
467
}
468
}
469
470
// perform any required parameter conversions
471
void AC_Circle::convert_parameters()
472
{
473
// PARAMETER_CONVERSION - Added: Jan-2026 for 4.7
474
475
// exit immediately if radius_m parameter is already configured
476
if (_radius_parm_m.configured()) {
477
return;
478
}
479
480
// convert parameters
481
static const AP_Param::ConversionInfo conversion_info[] = {
482
#if APM_BUILD_TYPE(APM_BUILD_ArduSub)
483
{ 60, 0, AP_PARAM_FLOAT, "CIRCLE_RADIUS_M" }, // CIRCLE_RADIUS moved to CIRCLE_RADIUS_M
484
#else
485
{ 104, 0, AP_PARAM_FLOAT, "CIRCLE_RADIUS_M" }, // CIRCLE_RADIUS moved to CIRCLE_RADIUS_M
486
#endif
487
};
488
AP_Param::convert_old_parameters_scaled(conversion_info, ARRAY_SIZE(conversion_info), 0.01, 0);
489
}
490
491