#include <AP_HAL/AP_HAL.h>1#include <AP_Math/AP_Math.h>2#include <AP_Terrain/AP_Terrain.h>3#include "AC_Circle.h"4#include <AP_Logger/AP_Logger.h>5#include <AP_Vehicle/AP_Vehicle_Type.h>67extern const AP_HAL::HAL& hal;89// Circle mode default and limit constants10#define AC_CIRCLE_RADIUS_M_DEFAULT 10.0f // Default circle radius in meters11#define AC_CIRCLE_RATE_DEFAULT 20.0f // Default circle turn rate in degrees per second. Positive = clockwise, negative = counter-clockwise.12#define AC_CIRCLE_ANGULAR_ACCEL_MIN 2.0f // Minimum angular acceleration in deg/s² (used to avoid sluggish yaw transitions).13#define AC_CIRCLE_RADIUS_MAX_M 2000.0 // Maximum allowed circle radius in meters (2000 m = 2 km).1415const AP_Param::GroupInfo AC_Circle::var_info[] = {16// 0 was RADIUS (cm)1718// @Param: RATE19// @DisplayName: Circle rate20// @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.21// @Units: deg/s22// @Range: -90 9023// @Increment: 124// @User: Standard25AP_GROUPINFO("RATE", 1, AC_Circle, _rate_parm_degs, AC_CIRCLE_RATE_DEFAULT),2627// @Param: OPTIONS28// @DisplayName: Circle options29// @Description: 0:Enable or disable using the pitch/roll stick control circle mode's radius and rate30// @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 circle31// @User: Standard32AP_GROUPINFO("OPTIONS", 2, AC_Circle, _options, 1),3334// @Param: RADIUS_M35// @DisplayName: Circle Radius36// @Description: Radius of the circle the vehicle will fly when in Circle flight mode37// @Units: m38// @Range: 0 200039// @Increment: 140// @User: Standard41AP_GROUPINFO("RADIUS_M", 3, AC_Circle, _radius_parm_m, AC_CIRCLE_RADIUS_M_DEFAULT),4243AP_GROUPEND44};4546// Default constructor.47// Note that the Vector/Matrix constructors already implicitly zero48// their values.49AC_Circle::AC_Circle(const AP_AHRS_View& ahrs, AC_PosControl& pos_control) :50_ahrs(ahrs),51_pos_control(pos_control)52{53AP_Param::setup_object_defaults(this, var_info);5455_rotation_rate_max_rads = radians(_rate_parm_degs);56}5758// Initializes circle flight using a center position in centimeters relative to the EKF origin.59// See init_NED_m() for full details.60void AC_Circle::init_NEU_cm(const Vector3p& center_neu_cm, bool is_terrain_alt, float rate_degs)61{62// Convert input from NEU cm to NED meters and delegate to meter-based initializer63Vector3p center_ned_m = Vector3p{center_neu_cm.x, center_neu_cm.y, -center_neu_cm.z} * 0.01;64init_NED_m(center_ned_m, is_terrain_alt, rate_degs);65}6667// Initializes circle flight mode using a specified center position in meters.68// Parameters:69// - center_ned_m: Center of the circle in NED frame (meters, relative to EKF origin)70// - is_terrain_alt: If true, center_ned_m.z is interpreted as relative to terrain; otherwise, above EKF origin71// - rate_degs: Desired turn rate in degrees per second (positive = clockwise, negative = counter-clockwise)72// Caller must preconfigure the position controller's speed and acceleration settings before calling.73void AC_Circle::init_NED_m(const Vector3p& center_ned_m, bool is_terrain_alt, float rate_degs)74{75// Store circle center and frame reference76_center_ned_m = center_ned_m;77_is_terrain_alt = is_terrain_alt;7879// Convert desired turn rate from degrees to radians80_rotation_rate_max_rads = radians(rate_degs);8182// Initialise position controller using current lean angles83_pos_control.NE_init_controller_stopping_point();84_pos_control.D_init_controller_stopping_point();8586// Calculate velocity and acceleration limits based on circle configuration87calc_velocities(true);8889// Use position-based angle initialization (avoids sharp yaw discontinuity)90init_start_angle(false);91}9293// Initializes the circle flight mode using the current stopping point as a reference.94// If the INIT_AT_CENTER option is not set, the circle center is projected one radius ahead along the vehicle's heading.95// Caller must configure position controller speeds and accelerations beforehand.96void AC_Circle::init()97{98// Load radius and rate from parameters99_radius_m = _radius_parm_m.get();100_last_radius_param_m = _radius_m;101_rotation_rate_max_rads = radians(_rate_parm_degs);102103// Initialise position controller using current lean angles104_pos_control.NE_init_controller_stopping_point();105_pos_control.D_init_controller_stopping_point();106107// Get stopping point as initial reference for center108const Vector3p& stopping_point_ned_m = _pos_control.get_pos_desired_NED_m();109110// By default, set center to stopping point111_center_ned_m = stopping_point_ned_m;112113// If INIT_AT_CENTER is not set, project center forward by radius in heading direction114if ((_options.get() & CircleOptions::INIT_AT_CENTER) == 0) {115_center_ned_m.x += _radius_m * _ahrs.cos_yaw();116_center_ned_m.y += _radius_m * _ahrs.sin_yaw();117}118119// Circle altitude is relative to EKF origin by default120_is_terrain_alt = false;121122// Calculate velocity and acceleration constraints123calc_velocities(true);124125// Initialize angle using vehicle heading126init_start_angle(true);127}128129// Sets the circle center using a Location object.130// Automatically determines whether the location uses terrain-relative or origin-relative altitude.131// If conversion fails, defaults to current position and logs a navigation error.132void AC_Circle::set_center(const Location& center)133{134// Check if the location uses terrain-relative altitude135if (center.get_alt_frame() == Location::AltFrame::ABOVE_TERRAIN) {136// convert Location with terrain altitude137Vector2p center_ne_m;138float terr_alt_m;139140// Attempt to convert XY and Z to NEU frame with terrain altitude141if (center.get_vector_xy_from_origin_NE_m(center_ne_m) && center.get_alt_m(Location::AltFrame::ABOVE_TERRAIN, terr_alt_m)) {142set_center_NED_m(Vector3p{center_ne_m.x, center_ne_m.y, -terr_alt_m}, true);143} else {144// Conversion failed: fall back to current position and log error145set_center_NED_m(_pos_control.get_pos_estimate_NED_m(), false);146LOGGER_WRITE_ERROR(LogErrorSubsystem::NAVIGATION, LogErrorCode::FAILED_CIRCLE_INIT);147}148} else {149// Handle alt-above-origin, alt-above-home, or absolute altitudes150Vector3p circle_center_ned_m;151if (!center.get_vector_from_origin_NED_m(circle_center_ned_m)) {152// Conversion failed: fall back to current position and log error153circle_center_ned_m = _pos_control.get_pos_estimate_NED_m();154LOGGER_WRITE_ERROR(LogErrorSubsystem::NAVIGATION, LogErrorCode::FAILED_CIRCLE_INIT);155}156157// Apply converted center and mark it as origin-relative158set_center_NED_m(circle_center_ned_m, false);159}160}161162// Sets the target circle rate in degrees per second.163// Positive values result in clockwise rotation; negative for counter-clockwise.164void AC_Circle::set_rate_degs(float rate_degs)165{166// Convert the rate from degrees/sec to radians/sec167_rotation_rate_max_rads = radians(rate_degs);168}169170// Sets the circle radius in centimeters.171// See set_radius_m() for full details.172void AC_Circle::set_radius_cm(float radius_cm)173{174// Convert input from cm to meters and call set_radius_m()175set_radius_m(radius_cm * 0.01);176}177178// Sets the circle radius in meters.179// Radius is constrained to AC_CIRCLE_RADIUS_MAX_M.180void AC_Circle::set_radius_m(float radius_m)181{182// Constrain the radius to prevent unsafe or invalid circle sizes183_radius_m = constrain_float(radius_m, 0, AC_CIRCLE_RADIUS_MAX_M);184}185186// Returns true if the circle controller's update() function has run recently.187// Used by vehicle code to determine if yaw and position outputs are valid.188bool AC_Circle::is_active() const189{190// Consider the controller active if the last update occurred within the past 200 milliseconds191return (AP_HAL::millis() - _last_update_ms < 200);192}193194// Updates the circle controller using a climb rate in cm/s.195// See update_ms() for full implementation details.196bool AC_Circle::update_cms(float climb_rate_cms)197{198// Convert climb rate from cm/s to m/s and call the meter-based update199return update_ms(climb_rate_cms * 0.01);200}201202// Updates the circle controller using a climb rate in m/s.203// Computes new angular position, yaw, and vertical trajectory, then updates the position controller.204// Returns false if terrain data is required but unavailable.205bool AC_Circle::update_ms(float climb_rate_ms)206{207// Recalculate angular velocities based on the current radius and rate208calc_velocities(false);209210// calculate dt211const float dt = _pos_control.get_dt_s();212213// ramp angular velocity to maximum214if (_angular_vel_rads < _angular_vel_max_rads) {215_angular_vel_rads += fabsf(_angular_accel_radss) * dt;216_angular_vel_rads = MIN(_angular_vel_rads, _angular_vel_max_rads);217}218if (_angular_vel_rads > _angular_vel_max_rads) {219_angular_vel_rads -= fabsf(_angular_accel_radss) * dt;220_angular_vel_rads = MAX(_angular_vel_rads, _angular_vel_max_rads);221}222223// update the target angle and total angle travelled224float angle_change_rad = _angular_vel_rads * dt;225_angle_rad += angle_change_rad;226_angle_rad = wrap_PI(_angle_rad);227_angle_total_rad += angle_change_rad;228229// calculate terrain adjustments230float terrain_u_m = 0.0f;231if (_is_terrain_alt && !get_terrain_U_m(terrain_u_m)) {232return false;233}234235// calculate z-axis target236float target_d_m;237if (_is_terrain_alt) {238target_d_m = _center_ned_m.z - terrain_u_m;239} else {240target_d_m = -_pos_control.get_pos_desired_U_m();241}242243// Construct target position centered on the circle center244Vector3p target_ned_m {245_center_ned_m.x,246_center_ned_m.y,247target_d_m248};249if (!is_zero(_radius_m)) {250// Calculate position on the circle edge based on current angle251target_ned_m.x += _radius_m * cosf(-_angle_rad);252target_ned_m.y += - _radius_m * sinf(-_angle_rad);253254// Compute yaw toward the circle center255_yaw_rad = get_bearing_rad(_pos_control.get_pos_desired_NED_m().xy().tofloat(), _center_ned_m.xy().tofloat());256257// Optionally adjust yaw to face direction of travel258if ((_options.get() & CircleOptions::FACE_DIRECTION_OF_TRAVEL) != 0) {259_yaw_rad += is_positive(_rotation_rate_max_rads) ? -radians(90.0) : radians(90.0);260_yaw_rad = wrap_2PI(_yaw_rad);261}262} else {263// set heading be the same as the angle for zero radius264_yaw_rad = _angle_rad;265}266267// update position controller target268Vector2f zero_ne;269_pos_control.input_pos_vel_accel_NE_m(target_ned_m.xy(), zero_ne, zero_ne);270if (_is_terrain_alt) {271float vel_zero = 0;272float target_pos_d_m = target_ned_m.z;273_pos_control.input_pos_vel_accel_D_m(target_pos_d_m, vel_zero, 0);274} else {275_pos_control.D_set_pos_target_from_climb_rate_ms(climb_rate_ms);276}277278// update position controller279_pos_control.NE_update_controller();280281// set update time282_last_update_ms = AP_HAL::millis();283284return true;285}286287// Returns the closest point on the circle to the vehicle's current position in centimeters.288// See get_closest_point_on_circle_NED_m() for full details.289void AC_Circle::get_closest_point_on_circle_NEU_cm(Vector3f& result_neu_cm, float& dist_cm) const290{291// Convert input arguments from neu cm to ned meters292Vector3p result_ned_m = Vector3p{result_neu_cm.x, result_neu_cm.y, -result_neu_cm.z} * 0.01;293float dist_m = dist_cm * 0.01;294295// Compute closest point in meters296get_closest_point_on_circle_NED_m(result_ned_m, dist_m);297298// Convert results back to neu centimeters299result_neu_cm = Vector3f(result_ned_m.x, result_ned_m.y, -result_ned_m.z) * 100.0;300dist_cm = dist_m * 100.0;301}302303// Returns the closest point on the circle to the vehicle's stopping point in meters.304// The result vector is updated with the NEU position of the closest point on the circle.305// The altitude (z) is set to match the circle center's altitude.306// dist_m is updated with the 3D distance to the circle edge from the stopping point.307// If the vehicle is at the center, the point directly behind the vehicle (based on yaw) is returned.308void AC_Circle::get_closest_point_on_circle_NED_m(Vector3p& result_ned_m, float& dist_to_edge_m) const309{310// Get vehicle stopping point from the position controller (in NEU frame, meters)311Vector3p stopping_point_ned_m;312_pos_control.get_stopping_point_NE_m(stopping_point_ned_m.xy());313_pos_control.get_stopping_point_D_m(stopping_point_ned_m.z);314315// Compute vector from stopping point to the circle center316Vector3f vec_from_center_ned_m = (stopping_point_ned_m - _center_ned_m).tofloat();317// Return circle center if radius is zero (vehicle orbits in place)318if (!is_positive(_radius_m)) {319result_ned_m = _center_ned_m;320dist_to_edge_m = 0;321return;322}323324const float dist_to_center_m_sq = vec_from_center_ned_m.length_squared();325// Handle edge case: vehicle is at the exact center of the circle326if (dist_to_center_m_sq < sq(0.5)) {327result_ned_m.x = _center_ned_m.x - _radius_m * _ahrs.cos_yaw();328result_ned_m.y = _center_ned_m.y - _radius_m * _ahrs.sin_yaw();329result_ned_m.z = _center_ned_m.z;330dist_to_edge_m = (stopping_point_ned_m - result_ned_m).length();331return;332}333334// Calculate the closest point on the circle's edge by projecting out from center335const float dist_to_center_m_xy = vec_from_center_ned_m.xy().length();336result_ned_m.x = _center_ned_m.x + vec_from_center_ned_m.x / dist_to_center_m_xy * _radius_m;337result_ned_m.y = _center_ned_m.y + vec_from_center_ned_m.y / dist_to_center_m_xy * _radius_m;338result_ned_m.z = _center_ned_m.z;339dist_to_edge_m = (stopping_point_ned_m - result_ned_m).length();340}341342// Calculates angular velocity and acceleration limits based on the configured radius and rate.343// Should be called whenever radius or rate changes.344// If `init_velocity` is true, resets angular velocity to zero (used on controller startup).345void AC_Circle::calc_velocities(bool init_velocity)346{347// If performing a panorama (radius is zero), use current yaw rate and enforce minimum acceleration348if (_radius_m <= 0) {349_angular_vel_max_rads = _rotation_rate_max_rads;350_angular_accel_radss = MAX(fabsf(_angular_vel_max_rads), radians(AC_CIRCLE_ANGULAR_ACCEL_MIN)); // reach maximum yaw velocity in 1 second351}else{352// Limit max horizontal speed based on radius and available acceleration353float vel_max_ms = MIN(_pos_control.NE_get_max_speed_ms(), safe_sqrt(0.5f*_pos_control.NE_get_max_accel_mss()*_radius_m));354355// Convert linear speed to angular velocity (rad/s)356_angular_vel_max_rads = vel_max_ms/_radius_m;357358// Constrain to requested circle rate359_angular_vel_max_rads = constrain_float(_rotation_rate_max_rads, -_angular_vel_max_rads, _angular_vel_max_rads);360361// Derive maximum angular acceleration362_angular_accel_radss = MAX(_pos_control.NE_get_max_accel_mss() / _radius_m, radians(AC_CIRCLE_ANGULAR_ACCEL_MIN));363}364365// Reset angular velocity to zero at initialization if requested366if (init_velocity) {367_angular_vel_rads = 0.0;368}369}370371// Sets the initial angle around the circle and resets the accumulated angle.372// If `use_heading` is true, uses vehicle heading to initialize angle for minimal yaw motion.373// If false, uses position relative to circle center to set angle.374void AC_Circle::init_start_angle(bool use_heading)375{376// Reset the accumulated angle traveled around the circle377_angle_total_rad = 0.0;378379// If radius is zero, we're doing a panorama—set angle to current yaw heading380if (_radius_m <= 0) {381_angle_rad = _ahrs.yaw;382return;383}384385// if use_heading is true386if (use_heading) {387// Initialize angle directly behind current heading to minimize yaw motion388_angle_rad = wrap_PI(_ahrs.yaw - M_PI);389} else {390// If vehicle is exactly at the center, init angle behind vehicle (prevent undefined bearing)391const Vector3p &curr_pos_desired_ned_m = _pos_control.get_pos_desired_NED_m();392if (is_equal(curr_pos_desired_ned_m.x, _center_ned_m.x) && is_equal(curr_pos_desired_ned_m.y, _center_ned_m.y)) {393_angle_rad = wrap_PI(_ahrs.yaw - M_PI);394} else {395// Calculate bearing from circle center to current position396float bearing_rad = atan2f(curr_pos_desired_ned_m.y - _center_ned_m.y, curr_pos_desired_ned_m.x - _center_ned_m.x);397_angle_rad = wrap_PI(bearing_rad);398}399}400}401402// Returns the expected source of terrain data for the circle controller.403// Used to determine whether terrain offset comes from rangefinder, terrain database, or is unavailable.404AC_Circle::TerrainSource AC_Circle::get_terrain_source() const405{406// Use rangefinder if available and enabled407if (_rangefinder_available) {408return AC_Circle::TerrainSource::TERRAIN_FROM_RANGEFINDER;409}410#if AP_TERRAIN_AVAILABLE411// Fallback to terrain database if available and enabled412const AP_Terrain *terrain = AP_Terrain::get_singleton();413if ((terrain != nullptr) && terrain->enabled()) {414return AC_Circle::TerrainSource::TERRAIN_FROM_TERRAINDATABASE;415} else {416return AC_Circle::TerrainSource::TERRAIN_UNAVAILABLE;417}418#else419// Terrain unavailable if no valid source420return AC_Circle::TerrainSource::TERRAIN_UNAVAILABLE;421#endif422}423424// Returns terrain offset in meters above the EKF origin at the current position.425// Positive values indicate terrain is above the EKF origin altitude.426// Terrain source may be rangefinder or terrain database.427bool AC_Circle::get_terrain_U_m(float& terrain_u_m)428{429// Determine terrain source and calculate offset accordingly430switch (get_terrain_source()) {431case AC_Circle::TerrainSource::TERRAIN_UNAVAILABLE:432return false;433case AC_Circle::TerrainSource::TERRAIN_FROM_RANGEFINDER:434if (_rangefinder_healthy) {435// Use last known healthy rangefinder terrain offset436terrain_u_m = _rangefinder_terrain_u_m;437return true;438}439return false;440case AC_Circle::TerrainSource::TERRAIN_FROM_TERRAINDATABASE:441#if AP_TERRAIN_AVAILABLE442float terr_alt_m = 0.0f;443AP_Terrain *terrain = AP_Terrain::get_singleton();444if (terrain != nullptr && terrain->height_above_terrain(terr_alt_m, true)) {445// Calculate offset from EKF origin altitude to terrain altitude446terrain_u_m = _pos_control.get_pos_estimate_U_m() - terr_alt_m;447return true;448}449#endif450return false;451}452453// This fallback should never be reached454return false;455}456457// Checks if the circle radius parameter has changed.458// If so, updates internal `_radius_m` and stores the new parameter value.459void AC_Circle::check_param_change()460{461// Check if the stored radius param has changed462if (!is_equal(_last_radius_param_m, _radius_parm_m.get())) {463// update internal radius and store new parameter value to detect future changes464_radius_m = _radius_parm_m.get();465_last_radius_param_m = _radius_m;466}467}468469// perform any required parameter conversions470void AC_Circle::convert_parameters()471{472// PARAMETER_CONVERSION - Added: Jan-2026 for 4.7473474// exit immediately if radius_m parameter is already configured475if (_radius_parm_m.configured()) {476return;477}478479// convert parameters480static const AP_Param::ConversionInfo conversion_info[] = {481#if APM_BUILD_TYPE(APM_BUILD_ArduSub)482{ 60, 0, AP_PARAM_FLOAT, "CIRCLE_RADIUS_M" }, // CIRCLE_RADIUS moved to CIRCLE_RADIUS_M483#else484{ 104, 0, AP_PARAM_FLOAT, "CIRCLE_RADIUS_M" }, // CIRCLE_RADIUS moved to CIRCLE_RADIUS_M485#endif486};487AP_Param::convert_old_parameters_scaled(conversion_info, ARRAY_SIZE(conversion_info), 0.01, 0);488}489490491