#include <AP_HAL/AP_HAL.h>1#include "AC_Loiter.h"2#include <AP_Vehicle/AP_Vehicle_Type.h>3#include <AC_Avoidance/AC_Avoid.h>45extern const AP_HAL::HAL& hal;67#define LOITER_SPEED_DEFAULT_MS 12.5 // Default horizontal loiter speed in m/s.8#define LOITER_SPEED_MIN_MS 0.2 // Minimum allowed horizontal loiter speed in m/s.9#define LOITER_ACCEL_MAX_DEFAULT_MSS 5.0 // Default maximum horizontal acceleration in loiter mode (m/s²).10#define LOITER_BRAKE_ACCEL_DEFAULT_MSS 2.5 // Default maximum braking acceleration when sticks are released (m/s²).11#define LOITER_BRAKE_JERK_DEFAULT_MSSS 5.0 // Default maximum jerk applied during braking transitions (m/s³).12#define LOITER_BRAKE_START_DELAY_DEFAULT_S 1.0 // Delay (in seconds) before braking begins after sticks are released.13#define LOITER_VEL_CORRECTION_MAX_MS 2.0 // Maximum speed (in m/s) used for correcting position errors in loiter.14#define LOITER_POS_CORRECTION_MAX_M 2.0 // Maximum horizontal position error allowed before correction (m).15#define LOITER_ACTIVE_TIMEOUT_MS 200 // Loiter is considered active if updated within the past 200 ms.16#define LOITER_DEFAULT_OPTIONS 1 // Enable Coordinated Turn by default.1718const AP_Param::GroupInfo AC_Loiter::var_info[] = {1920// @Param: ANG_MAX21// @DisplayName: Loiter pilot angle max22// @Description{Copter, Sub}: Loiter maximum pilot requested lean angle. Set to zero for 2/3 of PSC_ANGLE_MAX/ATC_ANGLE_MAX. The maximum vehicle lean angle is still limited by PSC_ANGLE_MAX/ATC_ANGLE_MAX23// @Description: Loiter maximum pilot requested lean angle. Set to zero for 2/3 of Q_P_ANGLE_MAX/Q_A_ANGLE_MAX. The maximum vehicle lean angle is still limited by Q_P_ANGLE_MAX/Q_A_ANGLE_MAX24// @Units: deg25// @Range: 0 4526// @Increment: 127// @User: Advanced28AP_GROUPINFO("ANG_MAX", 1, AC_Loiter, _angle_max_deg, 0.0f),2930// 2 was SPEED31// 3 was ACC_MAX32// 4 was BRK_ACCEL33// 5 was BRK_JERK3435// @Param: BRK_DELAY36// @DisplayName: Loiter brake start delay (in seconds)37// @Description: Loiter brake start delay (in seconds)38// @Units: s39// @Range: 0 240// @Increment: 0.141// @User: Advanced42AP_GROUPINFO("BRK_DELAY", 6, AC_Loiter, _brake_delay_s, LOITER_BRAKE_START_DELAY_DEFAULT_S),4344// @Param: OPTIONS45// @DisplayName: Loiter mode options46// @Description: Enables optional Loiter mode behaviors47// @Bitmask: 0: Enable Coordinated turns48// @User: Standard49AP_GROUPINFO("OPTIONS", 7, AC_Loiter, _options, LOITER_DEFAULT_OPTIONS),5051// @Param: SPEED_MS52// @DisplayName: Loiter Horizontal Maximum Speed53// @Description: Defines the maximum speed in m/s which the aircraft will travel horizontally while in loiter mode54// @Units: m/s55// @Range: 0.20 3556// @Increment: 0.0557// @User: Standard58AP_GROUPINFO("SPEED_MS", 8, AC_Loiter, _speed_max_ne_ms, LOITER_SPEED_DEFAULT_MS),5960// @Param: ACC_MAX_M61// @DisplayName: Loiter maximum correction acceleration62// @Description: Loiter maximum correction acceleration in m/s/s. Higher values cause the copter to correct position errors more aggressively.63// @Units: m/s/s64// @Range: 1 9.8165// @Increment: 0.0166// @User: Advanced67AP_GROUPINFO("ACC_MAX_M", 9, AC_Loiter, _accel_max_ne_mss, LOITER_ACCEL_MAX_DEFAULT_MSS),6869// @Param: BRK_ACC_M70// @DisplayName: Loiter braking acceleration71// @Description: Loiter braking acceleration in m/s/s. Higher values stop the copter more quickly when the stick is centered.72// @Units: m/s/s73// @Range: 0.25 2.574// @Increment: 0.0175// @User: Advanced76AP_GROUPINFO("BRK_ACC_M", 10, AC_Loiter, _brake_accel_max_mss, LOITER_BRAKE_ACCEL_DEFAULT_MSS),7778// @Param: BRK_JRK_M79// @DisplayName: Loiter braking jerk80// @Description: Loiter braking jerk in m/s/s/s. Higher values will remove braking faster if the pilot moves the sticks during a braking maneuver.81// @Units: m/s/s/s82// @Range: 5 5083// @Increment: 0.0184// @User: Advanced85AP_GROUPINFO("BRK_JRK_M", 11, AC_Loiter, _brake_jerk_max_msss, LOITER_BRAKE_JERK_DEFAULT_MSSS),8687AP_GROUPEND88};8990// Default constructor.91// Note that the Vector/Matrix constructors already implicitly zero92// their values.93AC_Loiter::AC_Loiter(const AP_AHRS_View& ahrs, AC_PosControl& pos_control, const AC_AttitudeControl& attitude_control) :94_ahrs(ahrs),95_pos_control(pos_control),96_attitude_control(attitude_control)97{98AP_Param::setup_object_defaults(this, var_info);99}100101// Sets the initial loiter target position in meters from the EKF origin.102// - position_ne_m: horizontal position in the NE frame, in meters.103// - Initializes internal control state including acceleration targets and feed-forward planning.104void AC_Loiter::init_target_m(const Vector2p& position_ne_m)105{106sanity_check_params();107108// Configure speed/accel limits in meters using internal parameter (_accel_max_ne_mss)109_pos_control.NE_set_correction_speed_accel_m(LOITER_VEL_CORRECTION_MAX_MS, _accel_max_ne_mss);110_pos_control.NE_set_pos_error_max_m(LOITER_POS_CORRECTION_MAX_M);111112// Reset controller state for stationary loiter113_pos_control.NE_init_controller_stopping_point();114115// Zero out desired and predicted accelerations and angles116_predicted_accel_ne_mss.zero();117_desired_accel_ne_mss.zero();118_predicted_euler_angle_rad.zero();119_brake_accel_mss = 0.0f;120121// Set position target for stationary loiter122_pos_control.set_pos_desired_NE_m(position_ne_m);123}124125// Initializes the loiter controller using the current position and velocity.126// Updates feed-forward velocity, predicted acceleration, and resets control state.127void AC_Loiter::init_target()128{129sanity_check_params();130131// Configure correction speed and acceleration limits (in m/s and m/s²)132_pos_control.NE_set_correction_speed_accel_m(LOITER_VEL_CORRECTION_MAX_MS, _accel_max_ne_mss);133_pos_control.NE_set_pos_error_max_m(LOITER_POS_CORRECTION_MAX_M);134135// Apply velocity smoothing: softly transitions target acceleration to zero136_pos_control.NE_relax_velocity_controller();137138// Initialize prediction state using current acceleration and lean angles139_predicted_accel_ne_mss = _pos_control.get_accel_target_NED_mss().xy();140_predicted_euler_angle_rad.x = _pos_control.get_roll_rad();141_predicted_euler_angle_rad.y = _pos_control.get_pitch_rad();142_predicted_euler_rate.zero();143_predicted_euler_accel.zero();144_brake_accel_mss = 0.0f;145}146147// Reduces loiter responsiveness for smoother descent during landing.148// Internally softens horizontal control gains.149void AC_Loiter::soften_for_landing()150{151_pos_control.NE_soften_for_landing();152}153154// Sets pilot desired acceleration using Euler angles in centidegrees.155// See set_pilot_desired_acceleration_rad() for full details.156void AC_Loiter::set_pilot_desired_acceleration_cd(float euler_roll_angle_cd, float euler_pitch_angle_cd)157{158set_pilot_desired_acceleration_rad(cd_to_rad(euler_roll_angle_cd), cd_to_rad(euler_pitch_angle_cd));159}160161// Sets pilot desired acceleration using Euler angles in radians.162// - Internally computes a smoothed acceleration vector based on predictive rate shaping.163// - Inputs: `euler_roll_angle_rad`, `euler_pitch_angle_rad` in radians.164// - Applies internal shaping using the current attitude controller dt.165void AC_Loiter::set_pilot_desired_acceleration_rad(float euler_roll_angle_rad, float euler_pitch_angle_rad)166{167const float dt_s = _attitude_control.get_dt_s();168169// convert our desired attitude to an acceleration vector assuming we are not accelerating vertically170const Vector3f desired_euler_rad {euler_roll_angle_rad, euler_pitch_angle_rad, _ahrs.yaw};171const Vector3f desired_accel_ned_mss = _pos_control.lean_angles_rad_to_accel_NED_mss(desired_euler_rad);172173_desired_accel_ne_mss.x = desired_accel_ned_mss.x;174_desired_accel_ne_mss.y = desired_accel_ned_mss.y;175176// Compute attitude error between desired and predicted lean angles177Vector2f angle_error_euler_rad(wrap_PI(euler_roll_angle_rad - _predicted_euler_angle_rad.x), wrap_PI(euler_pitch_angle_rad - _predicted_euler_angle_rad.y));178179// Predict roll/pitch rate required to achieve target attitude180_attitude_control.command_model_rate_predictor(angle_error_euler_rad, _predicted_euler_rate, _predicted_euler_accel, dt_s);181182// Update internal attitude estimate for next iteration183_predicted_euler_angle_rad += _predicted_euler_rate * dt_s;184185// Convert predicted angles into an acceleration vector for braking/shaping186const Vector3f predicted_euler_rad {_predicted_euler_angle_rad.x, _predicted_euler_angle_rad.y, _ahrs.yaw};187const Vector3f predicted_accel_ned_mss = _pos_control.lean_angles_rad_to_accel_NED_mss(predicted_euler_rad);188189_predicted_accel_ne_mss = predicted_accel_ned_mss.xy();190191if (loiter_option_is_set(LoiterOption::COORDINATED_TURN_ENABLED)) {192Vector3f target_ang_vel_rads = _attitude_control.get_attitude_target_ang_vel();193Vector3f desired_velocity_ms = _pos_control.get_vel_desired_NED_ms();194Vector2f turn_accel_ne_mss = Vector2f(-desired_velocity_ms.y * target_ang_vel_rads.z, desired_velocity_ms.x * target_ang_vel_rads.z);195_desired_accel_ne_mss += turn_accel_ne_mss;196_predicted_accel_ne_mss += turn_accel_ne_mss;197}198}199200// Calculates the expected stopping point based on current velocity and position in the NE frame.201// Result is returned in meters.202// Uses the position controller’s deceleration model.203void AC_Loiter::get_stopping_point_NE_m(Vector2f& stopping_point_ne_m) const204{205Vector2p stop_ne_m;206// Query stopping point from position controller in postype (float or double)207_pos_control.get_stopping_point_NE_m(stop_ne_m);208209// Convert from postype to float (Vector2f)210stopping_point_ne_m = stop_ne_m.tofloat();211}212213// Returns the maximum pilot-commanded lean angle in centidegrees.214// See get_angle_max_rad() for full details.215float AC_Loiter::get_angle_max_cd() const216{217// Convert radians to centidegrees218return rad_to_cd(get_angle_max_rad());219}220221// Returns the maximum pilot-commanded lean angle in radians.222// - If `_angle_max_deg` is zero, this returns 2/3 of the limiting PSC angle.223// - Otherwise, returns the minimum of `_angle_max_deg` and PSC’s configured angle limit.224float AC_Loiter::get_angle_max_rad() const225{226if (!is_positive(_angle_max_deg)) {227// Use 2/3 of the smallest system-wide max lean angle228return MIN(_attitude_control.lean_angle_max_rad(), _pos_control.get_lean_angle_max_rad()) * (2.0f / 3.0f);229}230// Use configured parameter (in degrees), constrained to PSC limit231return MIN(radians(_angle_max_deg), _pos_control.get_lean_angle_max_rad());232}233234// Runs the loiter control loop, computing desired acceleration and updating position control.235// If `avoidance_on` is true, velocity is adjusted using avoidance logic before being applied.236void AC_Loiter::update(bool avoidance_on)237{238// Calculate desired velocity using pilot inputs, braking, and drag239calc_desired_velocity(avoidance_on);240241// Run position controller to compute desired attitude and thrust242_pos_control.NE_update_controller();243}244245// Sets the maximum allowed horizontal loiter speed in m/s.246void AC_Loiter::set_speed_max_NE_ms(float speed_max_ne_ms)247{248_speed_max_ne_ms.set(MAX(speed_max_ne_ms, LOITER_SPEED_MIN_MS));249}250251// perform any required parameter conversions252void AC_Loiter::convert_parameters()253{254// PARAMETER_CONVERSION - Added: Jan-2026 for 4.7255256// return immediately if no conversion is needed257if (_speed_max_ne_ms.configured() || _accel_max_ne_mss.configured() || _brake_accel_max_mss.configured() || _brake_jerk_max_msss.configured()) {258return;259}260261#if APM_BUILD_TYPE(APM_BUILD_ArduPlane)262// convert QuadPlane parameters263static const AP_Param::ConversionInfo conversion_info[] = {264{ 205, 8379, AP_PARAM_FLOAT, "Q_LOIT_SPEED_MS" }, // Q_LOIT_SPEED moved to Q_LOIT_SPEED_MS265{ 205, 12475, AP_PARAM_FLOAT, "Q_LOIT_ACC_MAX_M" }, // Q_LOIT_ACC_MAX moved to Q_LOIT_ACC_MAX_M266{ 205, 16571, AP_PARAM_FLOAT, "Q_LOIT_BRK_ACC_M" }, // Q_LOIT_BRK_ACCEL moved to Q_LOIT_BRK_ACC_M267{ 205, 20667, AP_PARAM_FLOAT, "Q_LOIT_BRK_JRK_M" }, // Q_LOIT_BRK_JERK moved to Q_LOIT_BRK_JRK_M268};269#elif APM_BUILD_TYPE(APM_BUILD_ArduSub)270// convert Sub parameters271static const AP_Param::ConversionInfo conversion_info[] = {272{ 63, 2, AP_PARAM_FLOAT, "LOIT_SPEED_MS" }, // LOIT_SPEED moved to LOIT_SPEED_MS273{ 63, 3, AP_PARAM_FLOAT, "LOIT_ACC_MAX_M" }, // LOIT_ACC_MAX moved to LOIT_ACC_MAX_M274{ 63, 4, AP_PARAM_FLOAT, "LOIT_BRK_ACC_M" }, // LOIT_BRK_ACCEL moved to LOIT_BRK_ACC_M275{ 63, 5, AP_PARAM_FLOAT, "LOIT_BRK_JRK_M" }, // LOIT_BRK_JERK moved to LOIT_BRK_JRK_M276};277#else278// convert Copter parameters279static const AP_Param::ConversionInfo conversion_info[] = {280{ 105, 2, AP_PARAM_FLOAT, "LOIT_SPEED_MS" }, // LOIT_SPEED moved to LOIT_SPEED_MS281{ 105, 3, AP_PARAM_FLOAT, "LOIT_ACC_MAX_M" }, // LOIT_ACC_MAX moved to LOIT_ACC_MAX_M282{ 105, 4, AP_PARAM_FLOAT, "LOIT_BRK_ACC_M" }, // LOIT_BRK_ACCEL moved to LOIT_BRK_ACC_M283{ 105, 5, AP_PARAM_FLOAT, "LOIT_BRK_JRK_M" }, // LOIT_BRK_JERK moved to LOIT_BRK_JRK_M284};285#endif286287AP_Param::convert_old_parameters_scaled(conversion_info, ARRAY_SIZE(conversion_info), 0.01, 0);288}289290// Ensures internal parameters are within valid safety limits.291// Applies min/max constraints on speed and acceleration settings.292void AC_Loiter::sanity_check_params()293{294// Enforce minimum loiter speed295_speed_max_ne_ms.set(MAX(_speed_max_ne_ms, LOITER_SPEED_MIN_MS));296297// Clamp horizontal accel to lean-angle-limited max298_accel_max_ne_mss.set(MIN(_accel_max_ne_mss, GRAVITY_MSS * tanf(_attitude_control.lean_angle_max_rad())));299}300301bool AC_Loiter::loiter_option_is_set(LoiterOption option) const {302return (_options & int8_t(option)) != 0;303}304305// Updates feed-forward velocity using pilot-requested acceleration and braking logic.306// - Applies drag and braking forces when sticks are released.307// - Velocity is adjusted for fence/avoidance if enabled.308// - Resulting velocity and acceleration are sent to the position controller.309void AC_Loiter::calc_desired_velocity(bool avoidance_on)310{311float ekfGndSpdLimit_ms, ahrsControlScaleXY;312// Query EKF-imposed horizontal ground speed limit (e.g. for optical flow)313AP::ahrs().getControlLimits(ekfGndSpdLimit_ms, ahrsControlScaleXY);314315const float dt_s = _pos_control.get_dt_s();316317// Apply speed limit from LOITER_SPEED and EKF constraint318float gnd_speed_limit_ms = MIN(_speed_max_ne_ms, ekfGndSpdLimit_ms);319gnd_speed_limit_ms = MAX(gnd_speed_limit_ms, LOITER_SPEED_MIN_MS);320321// Determine acceleration limit based on maximum allowed lean angle322float pilot_acceleration_max_mss = angle_rad_to_accel_mss(get_angle_max_rad());323324// Check for invalid dt325if (is_negative(dt_s)) {326return;327}328329// Integrate predicted acceleration330Vector2f desired_vel_ne_ms = _pos_control.get_vel_desired_NED_ms().xy();331332// update the desired velocity using our predicted acceleration333desired_vel_ne_ms += _predicted_accel_ne_mss * dt_s;334335Vector2f loiter_accel_brake_mss;336float desired_speed_ms = desired_vel_ne_ms.length();337if (!is_zero(desired_speed_ms)) {338Vector2f desired_vel_norm = desired_vel_ne_ms / desired_speed_ms;339340// Apply drag: deceleration proportional to current velocity341float drag_decel_mss = pilot_acceleration_max_mss * desired_speed_ms / gnd_speed_limit_ms;342343// Determine braking acceleration based on stick release and delay timer344float loiter_brake_accel_mss = 0.0f;345if (_desired_accel_ne_mss.is_zero()) {346if ((AP_HAL::millis() - _brake_timer_ms) > _brake_delay_s * 1000.0) {347float brake_gain = _pos_control.NE_get_vel_pid().kP() * 0.5f;348loiter_brake_accel_mss = constrain_float(sqrt_controller(desired_speed_ms, brake_gain, _brake_jerk_max_msss, dt_s), 0.0f, _brake_accel_max_mss);349}350} else {351loiter_brake_accel_mss = 0.0f;352_brake_timer_ms = AP_HAL::millis();353}354355// Integrate jerk-limited brake acceleration356_brake_accel_mss += constrain_float(loiter_brake_accel_mss - _brake_accel_mss, -_brake_jerk_max_msss * dt_s, _brake_jerk_max_msss * dt_s);357loiter_accel_brake_mss = desired_vel_norm * _brake_accel_mss;358359// Update desired speed based on braking and drag360desired_speed_ms = MAX(desired_speed_ms - (drag_decel_mss + _brake_accel_mss) * dt_s, 0.0f);361desired_vel_ne_ms = desired_vel_norm * desired_speed_ms;362}363364// Apply braking acceleration to overall feed-forward acceleration365_desired_accel_ne_mss -= loiter_accel_brake_mss;366367// Apply final velocity magnitude constraint368float desired_vel_ms = desired_vel_ne_ms.length();369if (desired_vel_ms > gnd_speed_limit_ms) {370desired_vel_ne_ms = desired_vel_ne_ms * gnd_speed_limit_ms / desired_vel_ms;371}372373#if AP_AVOIDANCE_ENABLED && !APM_BUILD_TYPE(APM_BUILD_ArduPlane)374if (avoidance_on) {375// Apply fence/obstacle avoidance adjustments (velocity only)376// TODO: We need to also limit the _desired_accel_ne_mss377AC_Avoid *_avoid = AP::ac_avoid();378if (_avoid != nullptr) {379Vector3f avoidance_vel_ned_cms{desired_vel_ne_ms.x * 100.0, desired_vel_ne_ms.y * 100.0, 0.0f};380_avoid->adjust_velocity(avoidance_vel_ned_cms, _pos_control.NE_get_pos_p().kP(), _accel_max_ne_mss * 100.0, _pos_control.D_get_pos_p().kP(), _pos_control.D_get_max_accel_mss() * 100.0, dt_s);381desired_vel_ne_ms = avoidance_vel_ned_cms.xy() * 0.01;382}383}384#endif // !APM_BUILD_ArduPlane385386// Retrieve current desired position387Vector2p desired_pos_ned_m = _pos_control.get_pos_desired_NED_m().xy();388389// Integrate velocity to update desired position390desired_pos_ned_m += (desired_vel_ne_ms * dt_s).topostype();391392// Send updated position, velocity, and acceleration to the position controller393_pos_control.set_pos_vel_accel_NE_m(desired_pos_ned_m, desired_vel_ne_ms, _desired_accel_ne_mss);394}395396397