Precision Landing - Weber-State-UAV-Program/Documentation-2024-2025 GitHub Wiki

The precision landing custom mode is implemented using the ROS2-PX4 Interface Library. It is essentially a state machine with 5 main states as illustrated in the overall system FSM.

DroneStates drawio

All code for the precision landing mode can be found in the precision_land.cpp file in the projects code repository here

Precision Landing States

1. Going Home

The going home state is responsible to directing the drone to its recorded home position using the 'GoTo' setpoint types as supported by the ROS2-PX4 interface library.

    case State::GoingHome: {
        // go to home position with RTL height, with heading as direction of travel
        const Eigen::Vector3f home_position{_vehicle_home_position->localPosition().x(), _vehicle_home_position->localPosition().y(), _rtl_z};
        const Eigen::Vector2f vehicle_to_target_xy = home_position.head(2) -_vehicle_local_position->positionNed().head(2);
        const float heading_target_rad = atan2f(vehicle_to_target_xy(1), vehicle_to_target_xy(0));

        if (vehicle_to_target_xy.norm() < 0.1f) 
        {
          // stop caring about heading (the arctangent becomes undefined)
          _goto_setpoint->update(home_position);
        } 
        else 
        {
          _goto_setpoint->update(home_position, heading_target_rad);
        }

        //If we have reached our reported home position, tags should be in frame.
        if (positionReached(home_position, _goto_position_reached_error_threshold, _goto_velocity_reached_error_threshold)) {
          _state = State::ApproachingTarget;
        }
      }
      break;

The home position is accessed using a shared pointer. It is essentially recorded as soon as the external mode is registered with the flight copmuter. The drone is considered to have reached the home position when the magnitude of the difference between the drones current position, and its home position is less than the threshold configured in precision_land.yaml

2. Approach Target

The approaching target state is responsible for the initial correction of the GPS/Home position error by using the pose of the first detected tag in the frame. It uses 'GoTo' position setpoints similar to the Go Home state.

    case State::ApproachingTarget: {
      //check if we already have an approach target         
      if (!_approach_target_set) {
        
        Eigen::Vector2f velocity_setpoint = Eigen::Vector2f::Zero();
        logDeltaErrors(velocity_setpoint);

        //if a tag has been detected, set the approach target to the landing target position
        if (!checkTagTimeout()){
          _approach_target_position = Eigen::Vector3f(_landing_target_position.x(), _landing_target_position.y(), _vehicle_local_position->positionNed().z()); 
          _approach_target_set = true; 
          _goto_setpoint->update(_approach_target_position);
        }
        else{
          //Just stay put
          _trajectory_setpoint->update(Eigen::Vector3f::Zero()); 
        }
      }
      //if we have an approach target, go to it. 
      else
      {    
        _goto_setpoint->update(_approach_target_position);

        if(positionReached(_approach_target_position, _goto_position_reached_error_threshold, _goto_velocity_reached_error_threshold)){
              _state = State::CenteringTarget;
        }
      }
      break; 
    }

If there is no tag in frame at this point, the drone will hold position and await manual takeover or failsafe activation. In testing, the error returning to the home position was never enough to result in no tag being in frame. Once the position is reached within the threshold specified in precision_land.yaml we switch to the active centering state.

3. Center Target

The center target is the state where we switch from position set-points to velocity set-points generated by our PI control loop. Since the state machine runs at a frequency of 50hz, and a velocity set-point is calculated at each tick of the state machine, our control loop thus runs at 50 hz.

INFO We are running this control loop at 50hz. It is REQUIRED by PX4 that outpoints are updated with a rate of at least 2hz. Anything below will be considered unresponsive and the flight computer will failsafe.

    case State::CenteringTarget: {
        // Check if we have a recent tag detection
        if (!checkTagTimeout()) {
          Eigen::Vector2f velocity_setpoint = GetVelocitySetpoint(dt_s);

          //log delta errors
          logDeltaErrors(velocity_setpoint);

          _trajectory_setpoint->update(Eigen::Vector3f(velocity_setpoint.x(), velocity_setpoint.y(), 0.0)); 
          
          
          if (min_time_elapsed && positionReached(Eigen::Vector3f(_landing_target_position.x(), _landing_target_position.y(), _vehicle_local_position->positionNed().z()), _position_reached_error_threshold, _velocity_reached_error_threshold)) {
              _state = State::OrientingTarget;
          } 
        }
        else {
            //maintain current position
            _trajectory_setpoint->update(Eigen::Vector3f::Zero()); 
        }
        break;
      }

The GetVelocitySetpoint() function is called to calculated the x and y velocity setpoint. It is passed dt_s which is the time that has passed since it was last called. dt_s is required for the integral portion of this controller.

Eigen::Vector2f PrecisionLandDemo::GetVelocitySetpoint(float dt_s)
{
  //error between current position and target position
  float delta_x = _vehicle_local_position->positionNed().x() - _landing_target_position.x();
  float delta_y = _vehicle_local_position->positionNed().y() - _landing_target_position.y();
  
  // Update integral terms with clamping
  _integral_error_x = std::clamp(_integral_error_x + delta_x * dt_s, -_i_vel_clamp, _i_vel_clamp);
  _integral_error_y = std::clamp(_integral_error_y + delta_y * dt_s, -_i_vel_clamp, _i_vel_clamp);
  
  //scale by gains
  float Xp = delta_x * _p_vel_gain_x;
  float Yp = delta_y * _p_vel_gain_y;
  float Xi = _integral_error_x * _i_vel_gain_x;
  float Yi = _integral_error_y * _i_vel_gain_y;
  
  //we want to counteract that error, negate
  float vx = -1.0f * (Xp + Xi);
  float vy = -1.0f * (Yp + Yi);
  
  // clamp to max values
  vx = std::clamp(vx, -1.0f * _max_xy_velocity, _max_xy_velocity);
  vy = std::clamp(vy, -1.0f * _max_xy_velocity, _max_xy_velocity);
  
  return Eigen::Vector2f(vx, vy);
}

This is the implemented PI controller. The integral term is clamped to prevent integral windup, and the output velocity is clamped to restrict the response of the drone to relatively slow corrections to keep the AprilTags in frame.

4. Orient Target

The orient target state performs an initial orientation of the drone before beginning descent. This is to ensure that the drone yaw is at least roughly alight so that aggressive corrections are not required during descent.

    case State::OrientingTarget: {
      //if we dont have our yaw target based on a tag, just stay put
      if (!checkTagTimeout()){
        Eigen::Vector2f velocity_setpoint = GetVelocitySetpoint(dt_s);

        if (!_approach_target_yaw_set){
          _approach_target_yaw = _yaw_setpoint; 
          _approach_target_yaw_set = true; 
        }

        logDeltaErrors(velocity_setpoint);

        _trajectory_setpoint->update(Eigen::Vector3f(velocity_setpoint.x(), velocity_setpoint.y(), 0.0), std::nullopt, _approach_target_yaw); 
      }
      else 
      {
        // stay put. 
        _trajectory_setpoint->update(Eigen::Vector3f::Zero()); 
      }

      Eigen::Vector3f target_position(_landing_target_position.x(), 
                                     _landing_target_position.y(), 
                                     _vehicle_local_position->positionNed().z());

      //If we are centered, oriented, and have spent enough time in this state, descend.                 
      if (min_time_elapsed && 
          orientationReached(_approach_target_yaw) && 
          positionReached(target_position, _position_reached_error_threshold, _velocity_reached_error_threshold)) {
        _state = State::DescendingTarget;
      }
      break; 
    }

5. Descend Target

The descend target state couples all previous states into one. It continuously centers using the PI control loop, and adjusts the yaw based on the orientation of the latest tag detection.

    case State::DescendingTarget: {
        // Check if we have a recent tag detection
        if (!checkTagTimeout()) {
          Eigen::Vector2f velocity_setpoint = GetVelocitySetpoint(dt_s);

          //log delta errors
          logDeltaErrors(velocity_setpoint);

          //if we are already at the target orientation, no need to yaw. 
          if(!orientationReached(_yaw_setpoint)){
            _trajectory_setpoint->update(Eigen::Vector3f(velocity_setpoint.x(), velocity_setpoint.y(), _descend_velocity), std::nullopt, _yaw_setpoint); 
          }
          else{
            _trajectory_setpoint->update(Eigen::Vector3f(velocity_setpoint.x(), velocity_setpoint.y(), _descend_velocity)); 
          }
          
          if (_land_detected->landed()) {
              _state = State::Finished;
          } 
        }
        else {
            //maintain current position
            _trajectory_setpoint->update(Eigen::Vector3f::Zero()); 
        }
        break;
      }
    case State::Finished: {
      if (_enable_delta_error_logging && _log_file.is_open()) {
        _log_file.close();
      }
      ModeBase::completed(px4_ros2::Result::Success);        
      break;
    }

Once the flight computer indicates that a landing has been detected, we move into the finished state, which is essentially a placeholder for no action.

Apriltag Detection

The apriltag detection callback is executed each time an apriltag is detected by the apriltag_ros node. Tag detections are published to the /detections topic, to which the precision landing node is subscribed. Tags are prioritized based on their size. ID: 3 is the smallest tag, followed by ID: 4. IDs 1 and 2 are the same size, so the priority order of these detections does not matter.

void PrecisionLandDemo::detectionCallback(const apriltag_msgs::msg::AprilTagDetectionArray::SharedPtr msg)
{
  if (!msg->detections.empty()) {
    _last_detection_time = _node.get_clock()->now();

    //priority order of tags
    std::vector<int> priority_order = {3, 4, 1, 2};

    std::vector<int> tag_ids;
    for (const auto& detection : msg->detections) {
        tag_ids.push_back(detection.id);
    }

    int target_tag_id = -1;
    for (int id : priority_order) {
      if (std::find(tag_ids.begin(), tag_ids.end(), id) != tag_ids.end()) {
        target_tag_id = id;
        break;
      }
    }

    if (target_tag_id != -1) 
    {
      geometry_msgs::msg::TransformStamped transform;
      rclcpp::Time detection_time(msg->header.stamp.sec, msg->header.stamp.nanosec); 

      try{
        std::string target_frame = "target:" + std::to_string(target_tag_id);
        transform = _tf_buffer->lookupTransform(_local_frame, target_frame, detection_time, 20ms);
      }
      catch(const std::exception& e)
      {
        RCLCPP_ERROR(_node.get_logger(), "Error looking up transform: %s", e.what());
        return; 
      }

      //update the landing target position
      _landing_target_position = Eigen::Vector2f(transform.transform.translation.x, transform.transform.translation.y);
      
      //update yaw based on tag orientation
      _yaw_setpoint = px4_ros2::quaternionToYaw(Eigen::Quaternionf(transform.transform.rotation.w, transform.transform.rotation.x, transform.transform.rotation.y, transform.transform.rotation.z));

      //apply yaw offset
      _yaw_setpoint += px4_ros2::degToRad(_yaw_offset_deg); 

      _integral_error_x = 0.0f;
      _integral_error_y = 0.0f;
    }
  }
}

The full code for this node can be found here

⚠️ **GitHub.com Fallback** ⚠️