/**************************************************************************** * * Copyright (c) 2022 PX4 Development Team. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * 3. Neither the name PX4 nor the names of its contributors may be * used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * ****************************************************************************/ #include "autopilot_tester_follow_me.h" // #include #include "math_helpers.h" #include #include #include #include #include #include FollowTargetSimulator::FollowTargetSimulator(std::array initial_position_ned, mavsdk::Telemetry::GroundTruth home) : _position_ned(initial_position_ned), _home(home) { _velocity_ned[0] = 0.0f; _velocity_ned[1] = 0.0f; _velocity_ned[2] = 0.0f; } FollowTargetSimulator::~FollowTargetSimulator() { } void FollowTargetSimulator::update(float delta_t_s) { const float velocity_m_s = 2.0; _velocity_ned[0] = velocity_m_s; _velocity_ned[1] = 0.0; _position_ned[0] += _velocity_ned[0] * delta_t_s; _position_ned[1] += _velocity_ned[1] * delta_t_s; _position_ned[2] += _velocity_ned[2] * delta_t_s; _udpate_count++; } std::array FollowTargetSimulator::get_position_global(bool add_noise) { std::array pos_ned = _position_ned; if (add_noise) { unsigned seed = _udpate_count; std::default_random_engine generator(seed); std::normal_distribution distribution(0.0, 1.0); pos_ned[0] += distribution(generator); pos_ned[1] += distribution(generator); pos_ned[2] += distribution(generator); } CHECK(std::isfinite(_home.latitude_deg)); CHECK(std::isfinite(_home.longitude_deg)); const auto ct = CoordinateTransformation({_home.latitude_deg, _home.longitude_deg}); mavsdk::geometry::CoordinateTransformation::LocalCoordinate local_coordinate{pos_ned[0], pos_ned[1]}; mavsdk::geometry::CoordinateTransformation::GlobalCoordinate global_coordinate = ct.global_from_local(local_coordinate); std::array global_pos{global_coordinate.latitude_deg, global_coordinate.longitude_deg, pos_ned[2] + _home.absolute_altitude_m}; return global_pos; } std::array FollowTargetSimulator::get_position_ned(bool add_noise) { std::array pos_ned = _position_ned; if (add_noise) { unsigned seed = _udpate_count; std::default_random_engine generator(seed); std::normal_distribution distribution(0.0, pos_noise_std); pos_ned[0] += distribution(generator); pos_ned[1] += distribution(generator); pos_ned[2] += distribution(generator); } return pos_ned; } std::array FollowTargetSimulator::get_velocity_ned_noisy() { return get_velocity_ned(true); } std::array FollowTargetSimulator::get_velocity_ned_ground_truth() { return get_velocity_ned(false); } std::array FollowTargetSimulator::get_velocity_ned(bool add_noise) { std::array vel_ned = _velocity_ned; if (add_noise) { unsigned seed = _udpate_count; std::default_random_engine generator(seed); std::normal_distribution distribution(0.0, vel_noise_std); vel_ned[0] += distribution(generator); vel_ned[1] += distribution(generator); vel_ned[2] += distribution(generator); } return vel_ned; } std::array FollowTargetSimulator::get_position_ned_noisy() { return get_position_ned(true); } std::array FollowTargetSimulator::get_position_ground_truth_ned() { return get_position_ned(false); } std::array FollowTargetSimulator::get_position_global_noisy() { return get_position_global(true); } std::array FollowTargetSimulator::get_position_global_ground_truth() { return get_position_global(false); } void FollowTargetSimulator::check_follow_angle(FollowMe::Config config, std::array drone_pos_ned, std::array target_pos_ned, float tolerance) { // This check assumes that the target is travelling straight on the x-axis const float target_to_drone_offset_x = drone_pos_ned[0] - target_pos_ned[0]; const float target_to_drone_offset_y = drone_pos_ned[1] - target_pos_ned[1]; // Follow Angle is measured relative from the target's course (direction it is moving towards) const float target_to_drone_angle_expected_rad = config.follow_angle_deg * (M_PI / 180.0f); const float target_to_drone_offset_x_expected = config.follow_distance_m * cos(target_to_drone_angle_expected_rad); const float target_to_drone_offset_y_expected = config.follow_distance_m * sin(target_to_drone_angle_expected_rad); // Check that drone is following at an expected position within the tolerance error CHECK(fabsf(target_to_drone_offset_x - target_to_drone_offset_x_expected) < tolerance); CHECK(fabsf(target_to_drone_offset_y - target_to_drone_offset_y_expected) < tolerance); } void AutopilotTesterFollowMe::connect(const std::string uri) { AutopilotTester::connect(uri); auto system = get_system(); _follow_me.reset(new FollowMe(system)); } void AutopilotTesterFollowMe::straight_line_test(const bool stream_velocity) { // CONFIGURATION for the test const unsigned location_update_rate = 1; // [Hz] How often the GPS location update samples are generated const float position_error_tolerance = 11.0f; // [m] Position error tolerance in both X and Y direction const float follow_me_height_setting = 10.0f; // [m] Height above home position where the Drone will follow from // Start with simulated target on the same plane as drone's home position std::array start_location_ned = get_current_position_ned(); FollowTargetSimulator target_simulator(start_location_ned, getHome()); // Configure the Follow Me parameters FollowMe::Config config; config.follow_height_m = follow_me_height_setting; config.follow_distance_m = 8.0f; config.follow_angle_deg = 0.0f; // Ensure that tangential velocity control generates fast enough trajectory to be responsive config.max_tangential_vel_m_s = 15.0f; // Allow some time for mode switch sleep_for(std::chrono::milliseconds(1000)); // Start Follow Me CHECK(FollowMe::Result::Success == _follow_me->start()); // Allow some time for mode switch sleep_for(std::chrono::milliseconds(1000)); // task loop bool target_moving = false; bool perform_checks = false; // Simulate generating 75 different target location updates for (unsigned location_update_idx = 0; location_update_idx < 75 * location_update_rate; ++location_update_idx) { std::array target_pos_ned_ground_truth = target_simulator.get_position_ground_truth_ned(); std::array position_ned = get_current_position_ned(); const float distance_to_target = norm(diff(target_pos_ned_ground_truth, position_ned)); // poor-man's state machine if (location_update_idx < 5) { // Stream target location without moving } else if (location_update_idx == 5) { // Change config to Follow from 'Behind' perform_checks = false; config.follow_angle_deg = 180.0f; CHECK(FollowMe::Result::Success == _follow_me->set_config(config)); } else if (location_update_idx < 15) { // Move target for 10 samples and wait for steady state of drone target_moving = true; } else if (location_update_idx < 20) { // Perform positional checks in steady state for 5 samples perform_checks = true; } else if (location_update_idx == 20) { // Change config to follow from 'Front' perform_checks = false; config.follow_angle_deg = 0.0f; CHECK(FollowMe::Result::Success == _follow_me->set_config(config)); } else if (location_update_idx < 30) { // Move target for 10 samples and wait for steady state of drone } else if (location_update_idx < 35) { // Perform positional checks in steady state for 5 samples perform_checks = true; } else if (location_update_idx == 35) { // Change config to follow from 'Front right' perform_checks = false; config.follow_angle_deg = 45.0f; CHECK(FollowMe::Result::Success == _follow_me->set_config(config)); } else if (location_update_idx < 45) { // Move target for 10 samples and wait for steady state of drone } else if (location_update_idx < 55) { // Perform positional checks in steady state for 5 samples perform_checks = true; } else if (location_update_idx == 55) { // Change config to follow from 'Front Left' perform_checks = false; config.follow_angle_deg = -45.0f; CHECK(FollowMe::Result::Success == _follow_me->set_config(config)); } else if (location_update_idx < 65) { // Move target for 10 samples and wait for steady state of drone } else if (location_update_idx < 75) { // Perform positional checks in steady state for 10 samples perform_checks = true; } if (target_moving) { target_simulator.update(1.0f / location_update_rate); } if (perform_checks) { check_current_altitude(follow_me_height_setting); CHECK(distance_to_target <= config.follow_distance_m + position_error_tolerance); CHECK(distance_to_target >= config.follow_distance_m - position_error_tolerance); target_simulator.check_follow_angle(config, position_ned, target_pos_ned_ground_truth, position_error_tolerance); } // Construct follow-me message std::array global_coordinate = target_simulator.get_position_global_noisy(); FollowMe::TargetLocation target_location{}; target_location.latitude_deg = global_coordinate[0]; target_location.longitude_deg = global_coordinate[1]; target_location.absolute_altitude_m = global_coordinate[2]; if (stream_velocity) { std::array target_vel_ned = target_simulator.get_velocity_ned_noisy(); target_location.velocity_x_m_s = target_vel_ned[0]; target_location.velocity_y_m_s = target_vel_ned[1]; target_location.velocity_z_m_s = target_vel_ned[2]; } else { target_location.velocity_x_m_s = NAN; target_location.velocity_y_m_s = NAN; target_location.velocity_z_m_s = NAN; } // Send message and check result CHECK(FollowMe::Result::Success == _follow_me->set_target_location(target_location)); sleep_for(std::chrono::milliseconds(1000 / location_update_rate)); } CHECK(FollowMe::Result::Success == _follow_me->stop()); } void AutopilotTesterFollowMe::stream_velocity_only() { const unsigned loop_update_rate = 1; const float position_tolerance = 4.0f; // Configure follow-me FollowMe::Config config; config.follow_angle_deg = 180.0f; // Follow from behind CHECK(FollowMe::Result::Success == _follow_me->set_config(config)); // Allow some time for mode switch sleep_for(std::chrono::milliseconds(1000)); // Start Follow Me CHECK(FollowMe::Result::Success == _follow_me->start()); // Allow some time for mode switch sleep_for(std::chrono::milliseconds(1000)); std::array drone_initial_pos = get_current_position_ned(); // Start streaming velocity only. The drone should not move. for (unsigned i = 0; i < 15 * loop_update_rate; i++) { FollowMe::TargetLocation target_location{}; target_location.latitude_deg = NAN; target_location.longitude_deg = NAN; target_location.absolute_altitude_m = NAN; target_location.velocity_x_m_s = 2.0f; target_location.velocity_y_m_s = 1.0f; target_location.velocity_z_m_s = 0.5f; // Send message and check result CHECK(FollowMe::Result::Success == _follow_me->set_target_location(target_location)); sleep_for(std::chrono::milliseconds(1000 / loop_update_rate)); } // Check that drone is still close to initial position and has not moved much std::array drone_final_pos = get_current_position_ned(); const float distance_travelled = norm(diff(drone_initial_pos, drone_final_pos)); CHECK(distance_travelled < position_tolerance); } void AutopilotTesterFollowMe::rc_adjustment_test() { // CONFIGURATION const unsigned loop_update_rate = 50; // [Hz] const float follow_height_setting = 10.0f; // [m] const float follow_angle_setting = 0.0f; // [deg] const float follow_distance_setting = 10.0f; // [m] // The constants below are copied from the "FlightTaskAutoFollowTarget.hpp" to get a reference point for // how much change a RC adjustment is expected to bring for each follow me parameters // [m/s] Speed with which the follow distance will be adjusted by when commanded with deflection via RC command static constexpr float FOLLOW_DISTANCE_USER_ADJUST_SPEED = 2.0; // [m/s] Speed with which the follow height will be adjusted by when commanded with deflection via RC command static constexpr float FOLLOW_HEIGHT_USER_ADJUST_SPEED = 1.5; // [rad/s] Angular rate with which the follow distance will be adjusted by when commanded with full deflection via RC command static constexpr float FOLLOW_ANGLE_USER_ADJUST_SPEED = 1.5; // Start with simulated target on the same plane as drone's home position std::array start_location_ned = get_current_position_ned(); FollowTargetSimulator target_simulator(start_location_ned, getHome()); // Set Follow Me parameters FollowMe::Config config; config.follow_height_m = follow_height_setting; config.follow_distance_m = follow_distance_setting; config.follow_angle_deg = follow_angle_setting; CHECK(FollowMe::Result::Success == _follow_me->set_config(config)); // [deg] Get a single sample of target's GPS coordinate const std::array target_global_coordinate = target_simulator.get_position_global_ground_truth(); // Set TargetLocation as the sample to simulate a stationary target for a controlled RC adjustment test const FollowMe::TargetLocation target_location{}; target_location.latitude_deg = target_global_coordinate[0]; target_location.longitude_deg = target_global_coordinate[1]; target_location.absolute_altitude_m = target_global_coordinate[2]; // [m] Save the target's location in Local NED (in meters), assuming it is where drone is at in the beginning const std::array target_pos = get_current_position_ned(); // Start Follow-me CHECK(FollowMe::Result::Success == _follow_me->start()); std::array drone_initial_pos; // task loop for (unsigned i = 0; i <= 60 * loop_update_rate; ++i) { _follow_me->set_target_location(target_location); std::array current_drone_pos = get_current_position_ned(); if (i < 5 * loop_update_rate) { // For 5 seconds, give time for the drone to go to it's initial following position (front) CHECK(getManualControl()->set_manual_control_input(0.f, 0.f, 0.5f, 0.f) == ManualControl::Result::Success); } else if (i == 5 * loop_update_rate) { // At 5 second mark, record the current drone position as initial position drone_initial_pos = get_current_position_ned(); } else if (i < 8 * loop_update_rate) { // FOLLOW HEIGHT ADJUSTMENT // Command Throttle-up (Z = 1.0f) for 3 seconds CHECK(getManualControl()->set_manual_control_input(0.f, 0.f, 1.0f, 0.f) == ManualControl::Result::Success); } else if (i < 10 * loop_update_rate) { // Center the throttle for 2 seconds CHECK(getManualControl()->set_manual_control_input(0.f, 0.f, 0.5f, 0.f) == ManualControl::Result::Success); } else if (i == 10 * loop_update_rate) { // Check if altitude has increased at least half of the expected adjustment (Z is directed downwards, so flip the sign) CHECK(-(current_drone_pos[2] - drone_initial_pos[2]) > FOLLOW_HEIGHT_USER_ADJUST_SPEED * 3.0f * 0.5f); } else if (i < 13 * loop_update_rate) { // FOLLOW HEIGHT ADJUSTMENT // Command Pitch-down (= Forward) (X = 1.0f) for 3 seconds CHECK(getManualControl()->set_manual_control_input(1.0f, 0.f, 0.5f, 0.f) == ManualControl::Result::Success); } else if (i < 15 * loop_update_rate) { // Center the Pitch for 2 seconds CHECK(getManualControl()->set_manual_control_input(0.f, 0.f, 0.5f, 0.f) == ManualControl::Result::Success); } else if (i == 15 * loop_update_rate) { // Check if follow distance has increased at least half of the expected adjustment const float target_to_drone_x_diff = current_drone_pos[0] - target_pos[0]; const float target_to_drone_y_diff = current_drone_pos[1] - target_pos[1]; const float current_follow_distance = sqrt(sq(target_to_drone_x_diff) + sq(target_to_drone_y_diff)); CHECK(current_follow_distance > follow_distance_setting + FOLLOW_DISTANCE_USER_ADJUST_SPEED * 3.0f * 0.5f); } else if (i < 18 * loop_update_rate) { // FOLLOW ANGLE ADJUSTMENT // Command Roll-right (=Rightwards) (Y = 1.0f) for 3 seconds CHECK(getManualControl()->set_manual_control_input(0.f, 1.0f, 0.5f, 0.f) == ManualControl::Result::Success); } else if (i < 20 * loop_update_rate) { // Center the Roll for 2 seconds CHECK(getManualControl()->set_manual_control_input(0.f, 0.f, 0.5f, 0.f) == ManualControl::Result::Success); } else if (i == 20 * loop_update_rate) { // Check if follow angle has increased at least half of the expected adjustment // Since Roll-right corresponds to the follow angle increasing in clockwise direction const float target_to_drone_x_diff = current_drone_pos[0] - target_pos[0]; const float target_to_drone_y_diff = current_drone_pos[1] - target_pos[1]; const float current_follow_angle_rad = atan2f(target_to_drone_y_diff, target_to_drone_x_diff); CHECK(current_follow_angle_rad > follow_angle_setting * M_PI / 180.0f + FOLLOW_ANGLE_USER_ADJUST_SPEED * 3.0f * 0.5f); } sleep_for(std::chrono::milliseconds(1000 / loop_update_rate)); } }