Files
ardupilot/libraries/AP_Follow/AP_Follow.cpp
2026-01-28 19:17:49 +11:00

965 lines
37 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 <http://www.gnu.org/licenses/>.
*/
//==============================================================================
// AP_Follow Library
// Target Following Logic for ArduPilot Vehicles
//==============================================================================
//==============================================================================
// Includes
//==============================================================================
#include "AP_Follow_config.h"
#if AP_FOLLOW_ENABLED
#include <AP_HAL/AP_HAL.h>
#include "AP_Follow.h"
#include <ctype.h>
#include <stdio.h>
#include <AP_AHRS/AP_AHRS.h>
#include <AP_Logger/AP_Logger.h>
#include <GCS_MAVLink/GCS.h>
#include <AP_Vehicle/AP_Vehicle_Type.h>
#include <AP_Scheduler/AP_Scheduler.h>
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<float>(Location::AltFrame::ABSOLUTE)
#define AP_FOLLOW_DIST_MAX_DEFAULT 0 // zero = ignored
#else
#define AP_FOLLOW_ALT_TYPE_DEFAULT static_cast<float>(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(float(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 targets 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 targets 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 senders 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 targets 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 0360
_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