/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
//==============================================================================
// AP_Follow Library
// Target Following Logic for ArduPilot Vehicles
//==============================================================================
//==============================================================================
// Includes
//==============================================================================
#include "AP_Follow_config.h"
#if AP_FOLLOW_ENABLED
#include
#include "AP_Follow.h"
#include
#include
#include
#include
#include
#include
#include
extern const AP_HAL::HAL& hal;
//==============================================================================
// Constants and Definitions
//==============================================================================
#define AP_FOLLOW_TIMEOUT 3 // position estimate timeout in seconds
#define AP_FOLLOW_SYSID_TIMEOUT_MS 10000 // forget sysid we are following if we have not heard from them in 10 seconds
#define AP_FOLLOW_OFFSET_TYPE_NED 0 // offsets are in north-east-down frame
#define AP_FOLLOW_OFFSET_TYPE_RELATIVE 1 // offsets are relative to lead vehicle's heading
#define AP_FOLLOW_POS_P_DEFAULT 0.1f // position error gain default
#if APM_BUILD_TYPE(APM_BUILD_ArduPlane)
#define AP_FOLLOW_ALT_TYPE_DEFAULT static_cast(Location::AltFrame::ABSOLUTE)
#define AP_FOLLOW_DIST_MAX_DEFAULT 0 // zero = ignored
#else
#define AP_FOLLOW_ALT_TYPE_DEFAULT static_cast(Location::AltFrame::ABOVE_HOME)
#define AP_FOLLOW_DIST_MAX_DEFAULT 100
#endif
//==============================================================================
// Constructor
//==============================================================================
AP_Follow *AP_Follow::_singleton;
// table of user settable parameters
const AP_Param::GroupInfo AP_Follow::var_info[] = {
// @Param: _ENABLE
// @DisplayName: Follow enable/disable
// @Description: Enabled/disable following a target
// @Values: 0:Disabled,1:Enabled
// @User: Standard
AP_GROUPINFO_FLAGS("_ENABLE", 1, AP_Follow, _enabled, 0, AP_PARAM_FLAG_ENABLE),
// 2 is reserved for TYPE parameter
// @Param: _SYSID
// @DisplayName: Follow target's mavlink system id
// @Description: Follow target's mavlink system id
// @Range: 0 255
// @User: Standard
AP_GROUPINFO("_SYSID", 3, AP_Follow, _sysid, 0),
// 4 is reserved for MARGIN parameter
// @Param: _DIST_MAX
// @DisplayName: Follow distance maximum
// @Description: Follow distance maximum. If exceeded, the follow estimate will be considered invalid. If zero there is no maximum.
// @Units: m
// @Range: 0 1000
// @User: Standard
AP_GROUPINFO("_DIST_MAX", 5, AP_Follow, _dist_max_m, AP_FOLLOW_DIST_MAX_DEFAULT),
// @Param: _OFS_TYPE
// @DisplayName: Follow offset type
// @Description: Follow offset type
// @Values: 0:North-East-Down, 1:Relative to lead vehicle heading
// @User: Standard
AP_GROUPINFO("_OFS_TYPE", 6, AP_Follow, _offset_type, AP_FOLLOW_OFFSET_TYPE_NED),
// @Param: _OFS_X
// @DisplayName: Follow offsets in meters north/forward
// @Description: Follow offsets in meters north/forward. If positive, this vehicle fly ahead or north of lead vehicle. Depends on FOLL_OFS_TYPE
// @Range: -100 100
// @Units: m
// @Increment: 1
// @User: Standard
// @Param: _OFS_Y
// @DisplayName: Follow offsets in meters east/right
// @Description: Follow offsets in meters east/right. If positive, this vehicle will fly to the right or east of lead vehicle. Depends on FOLL_OFS_TYPE
// @Range: -100 100
// @Units: m
// @Increment: 1
// @User: Standard
// @Param: _OFS_Z
// @DisplayName: Follow offsets in meters down
// @Description: Follow offsets in meters down. If positive, this vehicle will fly below the lead vehicle
// @Range: -100 100
// @Units: m
// @Increment: 1
// @User: Standard
AP_GROUPINFO("_OFS", 7, AP_Follow, _offset_m, 0),
#if !(APM_BUILD_TYPE(APM_BUILD_Rover))
// @Param: _YAW_BEHAVE
// @DisplayName: Follow yaw behaviour
// @Description: Follow yaw behaviour
// @Values: 0:None,1:Face Lead Vehicle,2:Same as Lead vehicle,3:Direction of Flight
// @User: Standard
AP_GROUPINFO("_YAW_BEHAVE", 8, AP_Follow, _yaw_behave, 1),
#endif
// @Param: _POS_P
// @DisplayName: Follow position error P gain
// @Description: Follow position error P gain. Converts the difference between desired vertical speed and actual speed into a desired acceleration that is passed to the throttle acceleration controller
// @Range: 0.01 1.00
// @Increment: 0.01
// @User: Standard
AP_SUBGROUPINFO(_p_pos, "_POS_", 9, AP_Follow, AC_P),
#if !(APM_BUILD_TYPE(APM_BUILD_Rover))
// @Param: _ALT_TYPE
// @DisplayName: Follow altitude type
// @Description: Follow altitude type
// @Values: 0:absolute, 1:relative, 3:terrain
// @User: Standard
AP_GROUPINFO("_ALT_TYPE", 10, AP_Follow, _alt_type, AP_FOLLOW_ALT_TYPE_DEFAULT),
#endif
// @Param: _OPTIONS
// @DisplayName: Follow options
// @Description: Follow options bitmask
// @Values: 0:None,1: Mount Follows lead vehicle on mode enter
// @User: Standard
AP_GROUPINFO("_OPTIONS", 11, AP_Follow, _options, 0),
// @Param: _ACCEL_NE
// @DisplayName: Acceleration limit for the horizontal kinematic input shaping
// @Description: Acceleration limit of the horizontal kinematic path generation used to determine how quickly the estimate varies in velocity
// @Range: 0 5
// @Units: m/s/s
// @User: Advanced
AP_GROUPINFO("_ACCEL_NE", 12, AP_Follow, _accel_max_ne_mss, 2.5),
// @Param: _JERK_NE
// @DisplayName: Jerk limit for the horizontal kinematic input shaping
// @Description: Jerk limit of the horizontal kinematic path generation used to determine how quickly the estimate varies in acceleration
// @Range: 0 20
// @Units: m/s/s/s
// @User: Advanced
AP_GROUPINFO("_JERK_NE", 13, AP_Follow, _jerk_max_ne_msss, 5.0),
// @Param: _ACCEL_D
// @DisplayName: Acceleration limit for the vertical kinematic input shaping
// @Description: Acceleration limit of the vertical kinematic path generation used to determine how quickly the estimate varies in velocity
// @Range: 0 2.5
// @Units: m/s/s
// @User: Advanced
AP_GROUPINFO("_ACCEL_D", 14, AP_Follow, _accel_max_d_mss, 2.5),
// @Param: _JERK_D
// @DisplayName: Jerk limit for the vertical kinematic input shaping
// @Description: Jerk limit of the vertical kinematic path generation used to determine how quickly the estimate varies in acceleration
// @Range: 0 5
// @Units: m/s/s/s
// @User: Advanced
AP_GROUPINFO("_JERK_D", 15, AP_Follow, _jerk_max_d_msss, 5.0),
// @Param: _ACCEL_H
// @DisplayName: Angular acceleration limit for the heading kinematic input shaping
// @Description: Angular acceleration limit of the heading kinematic path generation used to determine how quickly the estimate varies in angular velocity
// @Range: 0 90
// @Units: deg/s/s
// @User: Advanced
AP_GROUPINFO("_ACCEL_H", 16, AP_Follow, _accel_max_h_degss, 90.0),
// @Param: _JERK_H
// @DisplayName: Angular jerk limit for the heading kinematic input shaping
// @Description: Angular jerk limit of the heading kinematic path generation used to determine how quickly the estimate varies in angular acceleration
// @Range: 0 360
// @Units: deg/s/s/s
// @User: Advanced
AP_GROUPINFO("_JERK_H", 17, AP_Follow, _jerk_max_h_degsss, 360.0),
// @Param: _TIMEOUT
// @DisplayName: Follow timeout
// @Description: Follow position update from lead - timeout after x seconds
// @User: Standard
// @Units: s
AP_GROUPINFO("_TIMEOUT", 18, AP_Follow, _timeout, AP_FOLLOW_TIMEOUT),
AP_GROUPEND
};
// Constructor for AP_Follow. Initializes the position P-controller and sets up parameter defaults.
AP_Follow::AP_Follow() :
_p_pos(AP_FOLLOW_POS_P_DEFAULT)
{
_singleton = this;
AP_Param::setup_object_defaults(this, var_info);
}
//==============================================================================
// Target Estimation Update Functions
//==============================================================================
// Projects and updates the estimated target position, velocity, and heading based on last known data and configured input shaping.
void AP_Follow::update_estimates()
{
WITH_SEMAPHORE(_follow_sem);
// check for target: if no valid target, invalidate estimate
if (!have_target()) {
clear_dist_and_bearing_to_target();
_estimate_valid = false;
return;
}
// if sysid changed, reset the estimation state
if (_sysid != _sysid_used) {
_sysid_used = _sysid;
_estimate_valid = false;
}
const uint32_t now = AP_HAL::millis();
// calculate time since last location update in seconds
const float dt = (now - _last_location_update_ms) * 0.001f;
// project target's position and velocity forward using simple kinematics
Vector3f delta_pos_m = _target_vel_ned_ms * dt + _target_accel_ned_mss * 0.5f * sq(dt);
Vector3f delta_vel_ms = _target_accel_ned_mss * dt;
float delta_heading_rad = radians(_target_heading_rate_degs) * dt;
// calculate time since last estimation update
const float e_dt = (now - _last_estimation_update_ms) * 0.001f;
const bool valid_kinematic_params = (_accel_max_ne_mss > 0.0f) && (_jerk_max_ne_msss > 0.0f) &&
(_accel_max_d_mss > 0.0f) && (_jerk_max_d_msss > 0.0f) &&
(_accel_max_h_degss > 0.0f) && (_jerk_max_h_degsss > 0.0f);
if (_estimate_valid && valid_kinematic_params) {
// update X/Y position, velocity, acceleration with shaping
update_pos_vel_accel_xy(_estimate_pos_ned_m.xy(), _estimate_vel_ned_ms.xy(), _estimate_accel_ned_mss.xy(), e_dt, Vector2f(), Vector2f(), Vector2f());
// update Z axis position, velocity, acceleration without shaping (direct update)
update_pos_vel_accel(_estimate_pos_ned_m.z, _estimate_vel_ned_ms.z, _estimate_accel_ned_mss.z, e_dt, 0.0, 0.0, 0.0);
// apply horizontal shaping to refine estimate toward projected target state
shape_pos_vel_accel_xy(_target_pos_ned_m.xy() + delta_pos_m.xy().topostype(), _target_vel_ned_ms.xy() + delta_vel_ms.xy(), _target_accel_ned_mss.xy(),
_estimate_pos_ned_m.xy(), _estimate_vel_ned_ms.xy(), _estimate_accel_ned_mss.xy(),
0.0, _accel_max_ne_mss, _jerk_max_ne_msss, e_dt, false);
// apply angular shaping for heading estimate
shape_angle_vel_accel(radians(_target_heading_deg) + delta_heading_rad, radians(_target_heading_rate_degs), 0.0,
_estimate_heading_rad, _estimate_heading_rate_rads, _estimate_heading_accel_radss,
0.0, radians(_accel_max_h_degss),
radians(_jerk_max_h_degsss), e_dt, false);
// update heading angle separately to maintain proper wrapping [-PI, PI]
postype_t estimate_heading_rad = _estimate_heading_rad;
update_pos_vel_accel(estimate_heading_rad, _estimate_heading_rate_rads, _estimate_heading_accel_radss, e_dt, 0.0, 0.0, 0.0);
_estimate_heading_rad = wrap_PI(estimate_heading_rad);
} else {
// no valid estimate yet: initialise from latest target position
_estimate_pos_ned_m = _target_pos_ned_m + delta_pos_m.topostype();
_estimate_vel_ned_ms = _target_vel_ned_ms + delta_vel_ms;
_estimate_accel_ned_mss = _target_accel_ned_mss;
_estimate_heading_rad = radians(_target_heading_deg) + delta_heading_rad;
_estimate_heading_rate_rads = radians(_target_heading_rate_degs);
_estimate_valid = true;
}
Vector3f offset_m = _offset_m.get();
// calculate estimated position and velocity with offsets applied
if (offset_m.is_zero() || (_offset_type == AP_FOLLOW_OFFSET_TYPE_NED)) {
// offsets are in NED frame: simple addition
_ofs_estimate_pos_ned_m = _estimate_pos_ned_m + offset_m.topostype();
_ofs_estimate_vel_ned_ms = _estimate_vel_ned_ms;
_ofs_estimate_accel_ned_mss = _estimate_accel_ned_mss;
} else {
// offsets are in FRD frame: rotate by heading
offset_m.xy().rotate(_estimate_heading_rad);
_ofs_estimate_pos_ned_m = _estimate_pos_ned_m + offset_m.topostype();
_ofs_estimate_vel_ned_ms = _estimate_vel_ned_ms;
_ofs_estimate_accel_ned_mss = _estimate_accel_ned_mss;
// with kinematic shaping of heading we can improve our offset velocity and acceleration of the offset
if (valid_kinematic_params) {
Vector3f offset_cross = offset_m.cross(Vector3f{0.0, 0.0, 1.0});
float offset_length_m = offset_m.length();
_ofs_estimate_vel_ned_ms += offset_cross * offset_length_m * _estimate_heading_rate_rads;
_ofs_estimate_accel_ned_mss += offset_cross * offset_length_m * _estimate_heading_accel_radss;
}
}
// update the distance and bearing to the target
update_dist_and_bearing_to_target();
_last_estimation_update_ms = now;
// Check if the target is within the maximum distance
Vector3p current_position_ned_m;
if (!AP::ahrs().get_relative_position_NED_origin(current_position_ned_m)) {
// no idea where we are; knowing where other things are won't help.
_estimate_valid = false;
return;
}
const Vector3p dist_vec_ned_m = _target_pos_ned_m - current_position_ned_m;
// If _dist_max_m is not positive, we don't check the distance
if (is_positive(_dist_max_m.get()) && (dist_vec_ned_m.length() > _dist_max_m)) {
// target is too far away, mark the estimate invalid
_estimate_valid = false;
}
}
//==============================================================================
// Target Information Retrieval Functions
//==============================================================================
// Retrieves the estimated target position, velocity, and acceleration in the NED frame (relative to origin).
bool AP_Follow::get_target_pos_vel_accel_NED_m(Vector3p &pos_ned_m, Vector3f &vel_ned_ms, Vector3f &accel_ned_mss) const
{
if (!_estimate_valid) {
return false;
}
pos_ned_m = _estimate_pos_ned_m;
vel_ned_ms = _estimate_vel_ned_ms;
accel_ned_mss = _estimate_accel_ned_mss;
return true;
}
// Retrieves the estimated target position, velocity, and acceleration in the NED frame, including configured offsets.
bool AP_Follow::get_ofs_pos_vel_accel_NED_m(Vector3p &pos_ofs_ned_m, Vector3f &vel_ofs_ned_ms, Vector3f &accel_ofs_ned_mss) const
{
if (!_estimate_valid) {
return false;
}
pos_ofs_ned_m = _ofs_estimate_pos_ned_m;
vel_ofs_ned_ms = _ofs_estimate_vel_ned_ms;
accel_ofs_ned_mss = _ofs_estimate_accel_ned_mss;
return true;
}
// Retrieves distance vectors (with and without configured offsets) and the target’s velocity, all in the NED frame.
bool AP_Follow::get_target_dist_and_vel_NED_m(Vector3f &dist_ned, Vector3f &dist_with_offs, Vector3f &vel_ned)
{
WITH_SEMAPHORE(_follow_sem);
if (!_estimate_valid) {
return false;
}
Vector3p current_position_ned_m;
if (!AP::ahrs().get_relative_position_NED_origin(current_position_ned_m)) {
return false;
}
const Vector3p dist_vec_ned_m = _estimate_pos_ned_m - current_position_ned_m;
const Vector3p ofs_dist_vec = _ofs_estimate_pos_ned_m - current_position_ned_m;
dist_ned = dist_vec_ned_m.tofloat();
dist_with_offs = ofs_dist_vec.tofloat();
vel_ned = _ofs_estimate_vel_ned_ms;
return true;
}
// Retrieves the estimated target heading and heading rate in radians.
bool AP_Follow::get_heading_heading_rate_rad(float &heading_rad, float &heading_rate_rads) const
{
if (!_estimate_valid) {
return false;
}
// return latest heading estimate
heading_rad = _estimate_heading_rad;
heading_rate_rads = _estimate_heading_rate_rads;
return true;
}
// Retrieves the target's estimated global location and estimated velocity
bool AP_Follow::get_target_location_and_velocity(Location &loc, Vector3f &vel_ned)
{
WITH_SEMAPHORE(_follow_sem);
if (!_estimate_valid) {
return false;
}
if (!AP::ahrs().get_location_from_origin_offset_NED(loc, _estimate_pos_ned_m)) {
return false;
}
vel_ned = _estimate_vel_ned_ms;
// The frame requested by FOLL_ALT_TYPE may not be the frame of location returned by ahrs.
// Make sure we give the caller the frame they have asked for.
return loc.change_alt_frame(_alt_type);
}
// Retrieves the target's estimated global location including configured offsets, and estimated velocity, for LUA bindings.
bool AP_Follow::get_target_location_and_velocity_ofs(Location &loc, Vector3f &vel_ned)
{
WITH_SEMAPHORE(_follow_sem);
if (!_estimate_valid) {
return false;
}
if (!AP::ahrs().get_location_from_origin_offset_NED(loc, _ofs_estimate_pos_ned_m)) {
return false;
}
vel_ned = _ofs_estimate_vel_ned_ms;
return true;
}
// Retrieves the estimated target heading in degrees (0° = North, 90° = East) for LUA bindings.
bool AP_Follow::get_target_heading_deg(float &heading_deg)
{
WITH_SEMAPHORE(_follow_sem);
if (!_estimate_valid) {
return false;
}
// return latest heading estimate
heading_deg = degrees(_estimate_heading_rad);
return true;
}
// Retrieves the estimated target heading in degrees (0° = North, 90° = East) for LUA bindings.
bool AP_Follow::get_target_heading_rate_degs(float &heading_rate_degs)
{
WITH_SEMAPHORE(_follow_sem);
if (!_estimate_valid) {
return false;
}
// return latest heading estimate
heading_rate_degs = degrees(_estimate_heading_rate_rads);
return true;
}
//==============================================================================
// MAVLink Message Handling
//==============================================================================
// Handles incoming MAVLink messages to update the target's position, velocity, and heading.
void AP_Follow::handle_msg(const mavlink_message_t &msg)
{
// Invalidate the estimate if no position update has been received within the timeout period.
// If using automatic sysid tracking, clear the sysid and reset tracking state.
if ((_last_location_update_ms == 0) ||
(AP_HAL::millis() - _last_location_update_ms > AP_FOLLOW_SYSID_TIMEOUT_MS)) {
if (_automatic_sysid) {
_sysid.set(0); // clear target system ID
_sysid_used = 0; // reset used sysid tracking
}
_estimate_valid = false; // mark estimate as invalid
_using_follow_target = false; // reset follow-target usage flag
}
if (!should_handle_message(msg)) {
// ignore message if filtering rules reject it (e.g., wrong sysid)
return;
}
// decode MAVLink message
bool updated = false;
switch (msg.msgid) {
case MAVLINK_MSG_ID_GLOBAL_POSITION_INT: {
// handle standard global position messages
updated = handle_global_position_int_message(msg);
break;
}
case MAVLINK_MSG_ID_FOLLOW_TARGET: {
// handle follow-target specific messages
updated = handle_follow_target_message(msg);
break;
}
}
if (updated) {
// Check if estimate needs reset based on position and velocity errors
if (estimate_error_too_large()) {
_estimate_valid = false;
}
#if HAL_LOGGING_ENABLED
// log current follow diagnostic data
Log_Write_FOLL();
#endif
}
}
// Returns true if the incoming MAVLink message should be processed for target updates.
bool AP_Follow::should_handle_message(const mavlink_message_t &msg) const
{
// exit immediately if not enabled
if (!_enabled) {
return false;
}
// skip our own messages
if (msg.sysid == mavlink_system.sysid) {
return false;
}
// skip message if not from our target
if (_sysid != 0 && msg.sysid != _sysid) {
return false;
}
return true;
}
// Checks whether the current estimate should be reset based on position and velocity errors.
bool AP_Follow::estimate_error_too_large() const
{
const float timeout_sec = _timeout;
// Calculate position thresholds based on maximum acceleration then deceleration for the timeout duration
const float pos_thresh_horiz_m = _accel_max_ne_mss.get() * sq(timeout_sec * 0.5);
const float pos_thresh_vert_m = _accel_max_d_mss.get() * sq(timeout_sec * 0.5);
// Calculate velocity thresholds using the new helper function
const float vel_thresh_horiz_ms = calc_max_velocity_change(_accel_max_ne_mss.get(), _jerk_max_ne_msss.get(), timeout_sec);
const float vel_thresh_vert_ms = calc_max_velocity_change(_accel_max_d_mss.get(), _jerk_max_d_msss.get(), timeout_sec);
// Calculate current position and velocity errors
const Vector3f pos_error = _estimate_pos_ned_m.tofloat() - _target_pos_ned_m.tofloat();
const Vector3f vel_error = _estimate_vel_ned_ms - _target_vel_ned_ms;
const Vector2f pos_error_xy = pos_error.xy();
const float pos_error_z = pos_error.z;
const Vector2f vel_error_xy = vel_error.xy();
const float vel_error_z = vel_error.z;
// Check horizontal and vertical separately
const bool pos_horiz_bad = pos_error_xy.length() > pos_thresh_horiz_m;
const bool vel_horiz_bad = vel_error_xy.length() > vel_thresh_horiz_ms;
const bool pos_vert_bad = fabsf(pos_error_z) > pos_thresh_vert_m;
const bool vel_vert_bad = fabsf(vel_error_z) > vel_thresh_vert_ms;
return pos_horiz_bad || vel_horiz_bad || pos_vert_bad || vel_vert_bad;
}
// Calculates max velocity change under trapezoidal or triangular acceleration profile (jerk-limited).
float AP_Follow::calc_max_velocity_change(float accel_max, float jerk_max, float timeout_sec) const
{
const float t_jerk = accel_max / jerk_max;
const float t_total_jerk = 2.0f * t_jerk;
if (timeout_sec >= t_total_jerk) {
// time to ramp up, constant accel phase, and ramp down
const float t_const = timeout_sec - t_total_jerk;
const float delta_v_jerk = 0.5f * accel_max * t_jerk;
const float delta_v_const = accel_max * t_const;
return 2.0f * delta_v_jerk + delta_v_const;
} else {
// timeout too short: pure triangle profile
const float t_half = timeout_sec * 0.5f;
return 0.5f * jerk_max * sq(t_half);
}
}
// Decodes a GLOBAL_POSITION_INT MAVLink message to update the target’s position, velocity, and heading.
bool AP_Follow::handle_global_position_int_message(const mavlink_message_t &msg)
{
// decode GLOBAL_POSITION_INT message into packet struct
mavlink_global_position_int_t packet;
mavlink_msg_global_position_int_decode(&msg, &packet);
// ignore message if latitude and longitude are exactly zero (invalid GPS fix)
if ((packet.lat == 0 && packet.lon == 0)) {
return false;
}
if (_using_follow_target) {
// if we are using follow_target, ignore global_position_int messages
return false;
}
Location target_location;
target_location.lat = packet.lat;
target_location.lng = packet.lon;
switch((Location::AltFrame)_alt_type) {
case Location::AltFrame::ABSOLUTE:
target_location.set_alt_cm(packet.alt * 0.1, Location::AltFrame::ABSOLUTE);
break;
case Location::AltFrame::ABOVE_HOME:
target_location.set_alt_cm(packet.relative_alt * 0.1, Location::AltFrame::ABOVE_HOME);
break;
#if APM_BUILD_TYPE(APM_BUILD_ArduPlane) || APM_BUILD_TYPE(APM_BUILD_ArduCopter)
case Location::AltFrame::ABOVE_TERRAIN:
/// Altitude comes in as AMSL
target_location.set_alt_cm(packet.alt * 0.1, Location::AltFrame::ABSOLUTE);
// convert the incoming altitude to terrain altitude, but fail if there is no terrain data available
if (!target_location.change_alt_frame(Location::AltFrame::ABOVE_TERRAIN)) {
return false;
};
break;
#endif
default:
// don't process the packet if the _alt_type is invalid
return false;
}
// convert global location to local NED frame position
Vector3p target_pos_neu_m;
if (!target_location.get_vector_from_origin_NEU_m(target_pos_neu_m)) {
return false;
}
if (packet.hdg <= 36000) {
// valid heading field available (in centi-degrees)
_target_heading_deg = packet.hdg * 0.01f;
} else if (_offset_type == AP_FOLLOW_OFFSET_TYPE_RELATIVE) {
// heading missing but relative offset mode requires heading -> reject
return false;
} else {
// no heading available: set heading rate to zero
_target_heading_rate_degs = 0.0f;
}
_target_pos_ned_m.xy() = target_pos_neu_m.xy();
_target_pos_ned_m.z = -target_pos_neu_m.z;
// decode target velocity components (cm/s converted to m/s)
_target_vel_ned_ms.x = packet.vx * 0.01f; // velocity north
_target_vel_ned_ms.y = packet.vy * 0.01f; // velocity east
_target_vel_ned_ms.z = packet.vz * 0.01f; // velocity down
// target acceleration not available in GLOBAL_POSITION_INT
_target_accel_ned_mss.zero();
// apply jitter-corrected timestamp to this update
_last_location_update_ms = _jitter.correct_offboard_timestamp_msec(packet.time_boot_ms, AP_HAL::millis());
// if sysid not yet set, adopt sender’s sysid and enable automatic sysid tracking
if (_sysid == 0) {
_sysid.set(msg.sysid);
_sysid_used = 0;
_estimate_valid = false;
_automatic_sysid = true;
}
return true;
}
// Decodes a FOLLOW_TARGET MAVLink message to update the target’s position, velocity, acceleration, and orientation.
bool AP_Follow::handle_follow_target_message(const mavlink_message_t &msg)
{
// decode FOLLOW_TARGET message into packet struct
mavlink_follow_target_t packet;
mavlink_msg_follow_target_decode(&msg, &packet);
// ignore message if latitude and longitude are exactly zero (invalid GPS fix)
if ((packet.lat == 0 && packet.lon == 0)) {
return false;
}
// require that at least position is estimated (bit 0 of est_capabilities)
if ((packet.est_capabilities & (1<<0)) == 0) {
return false;
}
// build Location object from latitude, longitude, and altitude (alt in meters)
const Location target_location {
packet.lat,
packet.lon,
int32_t(packet.alt * 100), // convert meters to centimeters
Location::AltFrame::ABSOLUTE
};
// convert global location to local NED frame position
Vector3p target_pos_neu_m;
if (!target_location.get_vector_from_origin_NEU_m(target_pos_neu_m)) {
return false;
}
// adjust Z coordinate to NED frame (NEU altitude -> NED)
Location origin;
if (!AP::ahrs().get_origin(origin)) {
return false;
}
// decode attitude if available (bit 3 of est_capabilities)
if (packet.est_capabilities & (1 << 3)) {
// reconstruct quaternion from packet
Quaternion q{packet.attitude_q[0], packet.attitude_q[1], packet.attitude_q[2], packet.attitude_q[3]};
float roll, pitch, yaw;
q.to_euler(roll, pitch, yaw);
// store heading in degrees, wrapped 0–360
_target_heading_deg = wrap_360(degrees(yaw));
// transform body rates (roll, pitch, yaw) to earth frame rates
Matrix3f R;
q.rotation_matrix(R);
Vector3f omega_body = Vector3f{packet.rates[0], packet.rates[1], packet.rates[2]};
Vector3f omega_world = R * omega_body; // rotate rates into earth frame
// store heading rate (yaw rate in world frame) in degrees/sec
_target_heading_rate_degs = degrees(omega_world.z);
} else if (_offset_type == AP_FOLLOW_OFFSET_TYPE_RELATIVE) {
// if attitude unavailable and using relative frame, cannot compute — abort
return false;
} else {
// otherwise, default heading rate to zero
_target_heading_rate_degs = 0.0f;
}
_target_pos_ned_m.xy() = target_pos_neu_m.xy();
_target_pos_ned_m.z = -packet.alt + origin.alt * 0.01;
// decode velocity if available (bit 1 of est_capabilities)
if (packet.est_capabilities & (1<<1)) {
_target_vel_ned_ms.x = packet.vel[0]; // velocity north
_target_vel_ned_ms.y = packet.vel[1]; // velocity east
_target_vel_ned_ms.z = packet.vel[2]; // velocity down
} else {
_target_vel_ned_ms.zero();
}
// decode acceleration if available (bit 2 of est_capabilities)
if (packet.est_capabilities & (1 << 2)) {
_target_accel_ned_mss.x = packet.acc[0]; // acceleration north
_target_accel_ned_mss.y = packet.acc[1]; // acceleration east
_target_accel_ned_mss.z = packet.acc[2]; // acceleration down
} else {
_target_accel_ned_mss.zero();
}
// apply jitter-corrected timestamp to this update
_last_location_update_ms = _jitter.correct_offboard_timestamp_msec(packet.timestamp, AP_HAL::millis());
// if sysid not yet assigned, adopt sender's sysid and enable automatic sysid tracking
if (_sysid == 0) {
_sysid.set(msg.sysid);
_automatic_sysid = true;
}
// we are using follow_target: set sysid to sender's sysid
_using_follow_target = true;
return true;
}
//==============================================================================
// Offset Initialization and Adjustment Functions
//==============================================================================
// Initializes the positional offsets from the target vehicle if not already set.
void AP_Follow::init_offsets_if_required()
{
// return immediately if offsets have already been set
if (!_offset_m.get().is_zero()) {
return;
}
_offsets_were_zero = true;
if (!_estimate_valid) {
return;
}
// Check if the target is within the maximum distance
Vector3p current_position_ned_m;
if (!AP::ahrs().get_relative_position_NED_origin(current_position_ned_m)) {
return;
}
const Vector3f dist_vec_ned_m = (_target_pos_ned_m - current_position_ned_m).tofloat();
if ((_offset_type == AP_FOLLOW_OFFSET_TYPE_RELATIVE)) {
// rotate offset into vehicle-relative frame based on heading
_offset_m.set(rotate_vector(-dist_vec_ned_m, -degrees(_estimate_heading_rad)));
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Relative follow offset loaded");
} else {
// initialize offset in NED frame (no heading rotation)
_offset_m.set(-dist_vec_ned_m);
// ensure offset type is set to NED frame if initialized this way
_offset_type.set(AP_FOLLOW_OFFSET_TYPE_NED);
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "N-E-D follow offset loaded");
}
}
// Rotates a 3D vector clockwise by the specified angle (degrees).
Vector3f AP_Follow::rotate_vector(const Vector3f &vec, float angle_deg) const
{
// rotate roll, pitch input from north facing to vehicle's perspective
Vector3f ret = vec;
ret.xy().rotate(radians(angle_deg));
return ret;
}
// Resets follow mode offsets to zero if they were automatically initialized.
void AP_Follow::clear_offsets_if_required()
{
if (_offsets_were_zero) {
_offset_m.set(Vector3f());
_offsets_were_zero = false;
}
}
//==============================================================================
// Distance and Bearing Management
//==============================================================================
// Resets the recorded distance and bearing to the target to zero.
void AP_Follow::clear_dist_and_bearing_to_target()
{
_dist_to_target_m = 0.0f;
_bearing_to_target_deg = 0.0f;
}
// Updates the recorded distance and bearing to the target to zero.
void AP_Follow::update_dist_and_bearing_to_target()
{
Vector3p current_position_ned_m;
if (!AP::ahrs().get_relative_position_NED_origin(current_position_ned_m)) {
// if unable to retrieve local position, clear distance/bearing info
clear_dist_and_bearing_to_target();
} else {
// convert vehicle position to NED meters (NEU -> NED and cm -> m)
current_position_ned_m.z = -current_position_ned_m.z; // NEU to NED
current_position_ned_m *= 0.01; // convert cm to m
// calculate distance vectors to target, both with and without offsets
const Vector3p ofs_dist_vec = _ofs_estimate_pos_ned_m - current_position_ned_m;
// record distance and bearing to target for reporting/logging
if (ofs_dist_vec.xy().is_zero()) {
clear_dist_and_bearing_to_target();
} else {
_dist_to_target_m = ofs_dist_vec.xy().length();
_bearing_to_target_deg = degrees(ofs_dist_vec.xy().angle());
}
}
}
//==============================================================================
// Logging
//==============================================================================
// Writes a diagnostic onboard log message containing target and vehicle tracking data for Follow mode.
#if HAL_LOGGING_ENABLED
void AP_Follow::Log_Write_FOLL()
{
// retrieve latest estimated location and velocity
Location loc_estimate{};
Vector3f vel_estimate;
UNUSED_RESULT(get_target_location_and_velocity(loc_estimate, vel_estimate));
Location target_location;
UNUSED_RESULT(AP::ahrs().get_location_from_origin_offset_NED(target_location, _target_pos_ned_m));
// log the lead target's reported position and vehicle's estimated position
// @LoggerMessage: FOLL
// @Description: Follow library diagnostic data
// @Field: TimeUS: Time since system startup (microseconds)
// @Field: Lat: Target latitude (degrees * 1E7)
// @Field: Lon: Target longitude (degrees * 1E7)
// @Field: Alt: Target absolute altitude (centimeters)
// @Field: VelN: Target velocity, North (m/s)
// @Field: VelE: Target velocity, East (m/s)
// @Field: VelD: Target velocity, Down (m/s)
// @Field: LatE: Vehicle estimated latitude (degrees * 1E7)
// @Field: LonE: Vehicle estimated longitude (degrees * 1E7)
// @Field: AltE: Vehicle estimated altitude (centimeters)
// @Field: FrmE: Vehicle estimated altitude Frame
AP::logger().WriteStreaming("FOLL",
"TimeUS,Lat,Lon,Alt,VelN,VelE,VelD,LatE,LonE,AltE,FrmE", // labels
"sDUmnnnDUm-", // units
"F--B000--B-", // mults
"QLLifffLLib", // fmt
AP_HAL::micros64(),
target_location.lat,
target_location.lng,
target_location.alt,
(double)_target_vel_ned_ms.x,
(double)_target_vel_ned_ms.y,
(double)_target_vel_ned_ms.z,
loc_estimate.lat,
loc_estimate.lng,
loc_estimate.alt,
loc_estimate.get_alt_frame()
);
}
#endif // HAL_LOGGING_ENABLED
//==============================================================================
// Accessors and Helpers
//==============================================================================
// Returns true if following is enabled and a recent target update has been received.
bool AP_Follow::have_target(void) const
{
if (!_enabled) {
return false;
}
// check for timeout
if ((_last_location_update_ms == 0) || ((AP_HAL::millis() - _last_location_update_ms) > (uint32_t)(_timeout * 1000.0f))) {
return false;
}
return true;
}
//==============================================================================
// AP_Follow Accessor
//==============================================================================
// Accessor for the AP_Follow singleton instance.
namespace AP {
AP_Follow &follow()
{
return *AP_Follow::get_singleton();
}
}
#endif // AP_FOLLOW_ENABLED