-
Notifications
You must be signed in to change notification settings - Fork 1
Add tailsitter heading alignment phase during forward transition (Pr yaw alignment) #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: airbound-develop
Are you sure you want to change the base?
Changes from all commits
b095445
d02251d
52a45a1
b38f8a4
b50e960
61d6167
59d8ee0
e6e30ea
5f386e0
8bf6929
5e5c25e
3ef3824
136ec4e
7ba3056
babd278
e578002
7a31a29
04fb2b3
0386c04
9d12039
3f4e94a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -986,6 +986,133 @@ void Tailsitter_Transition::update() | |||
| switch (transition_state) { | ||||
|
|
||||
| case TRANSITION_ANGLE_WAIT_FW: { | ||||
|
|
||||
| const uint32_t now_ = AP_HAL::millis(); | ||||
| // ----------------------------------------------------------- | ||||
| // 1. INITIALIZATION | ||||
| // ----------------------------------------------------------- | ||||
| quadplane.set_desired_spool_state(AP_Motors::DesiredSpoolState::THROTTLE_UNLIMITED); | ||||
|
|
||||
| // ----------------------------------------------------------- | ||||
| // 2. TAILSITTER HEADING ALIGNMENT & WAIT | ||||
| // ----------------------------------------------------------- | ||||
| if (quadplane.tailsitter.enabled()) { | ||||
|
|
||||
| // Constants | ||||
| const int32_t ALIGN_TOLERANCE_CD = 2000; // 20.0 degrees | ||||
| const float MAX_SPIN_RATE_DEG = 8.0f; // Max yaw rate allowed | ||||
| const uint32_t ALIGN_PHASE_LIMIT_MS = 6000; // 6s Total Timeout | ||||
| const uint32_t WAIT_DELAY_MS = 1000; // 1s Wait | ||||
|
|
||||
| // Static Variables (State Tracking) | ||||
| static uint32_t align_phase_start_ms = 0; | ||||
| static uint32_t alignment_done_ms = 0; | ||||
| static uint32_t last_run_ms = 0; | ||||
| static uint32_t last_log_ms = 0; | ||||
| static int32_t target_bearing_cd = 0; | ||||
| static bool alignment_completed_for_this_flight = false; | ||||
| static bool target_bearing_latched = false; | ||||
|
|
||||
| // --- DETECT NEW FLIGHT/TRANSITION ENTRY --- | ||||
| // If this function hasn't run for >200ms, assume it's a new attempt. | ||||
| if (now_ - last_run_ms > 200) { | ||||
| align_phase_start_ms = now_; | ||||
| alignment_done_ms = 0; | ||||
| last_log_ms = 0; | ||||
| alignment_completed_for_this_flight = false; // Reset flag for new transition | ||||
| target_bearing_latched = false; | ||||
| target_bearing_cd = 0; | ||||
| } | ||||
| last_run_ms = now_; | ||||
|
greptile-apps[bot] marked this conversation as resolved.
|
||||
|
|
||||
| // Check Validity & Only run if we haven't finished aligning yet | ||||
| bool should_run_alignment = !alignment_completed_for_this_flight && | ||||
| (plane.control_mode == &plane.mode_auto || plane.control_mode == &plane.mode_guided) && | ||||
| (plane.nav_controller != nullptr); | ||||
|
|
||||
| if (should_run_alignment) { | ||||
| if (!target_bearing_latched) { | ||||
| target_bearing_cd = plane.prev_WP_loc.get_bearing_to(plane.next_WP_loc); | ||||
| quadplane.attitude_control->reset_rate_controller_I_terms(); | ||||
| gcs().send_text(MAV_SEVERITY_INFO, "Alignment start: Target Heading %.1f", | ||||
| target_bearing_cd * 0.01f); | ||||
| target_bearing_latched = true; | ||||
| } | ||||
|
greptile-apps[bot] marked this conversation as resolved.
|
||||
| int32_t current_yaw_cd = quadplane.ahrs_view->yaw_sensor; | ||||
| int32_t error_cd = wrap_180_cd(target_bearing_cd - current_yaw_cd); | ||||
| Vector3f gyro = quadplane.ahrs.get_gyro(); | ||||
| float yaw_rate_deg = degrees(gyro.x); | ||||
|
|
||||
| // LOGIC: Are we aligned right now? | ||||
| bool is_aligned = (abs(error_cd) <= ALIGN_TOLERANCE_CD) && (abs(yaw_rate_deg) <= MAX_SPIN_RATE_DEG); | ||||
|
|
||||
| // TIMER LOGIC | ||||
| if (is_aligned) { | ||||
| if (alignment_done_ms == 0) alignment_done_ms = now_; | ||||
| } else { | ||||
| alignment_done_ms = 0; // Reset if we drift out | ||||
| } | ||||
|
|
||||
| // EXIT CRITERIA | ||||
| bool wait_complete = (alignment_done_ms != 0) && (now_ - alignment_done_ms >= WAIT_DELAY_MS); | ||||
| bool timeout_expired = (now_ - align_phase_start_ms >= ALIGN_PHASE_LIMIT_MS); | ||||
|
|
||||
|
Comment on lines
+1007
to
+1059
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Start the alignment timeout only when alignment is actually active. 💡 One way to gate the timer- // --- DETECT NEW FLIGHT/TRANSITION ENTRY ---
- // If this function hasn't run for >200ms, assume it's a new attempt.
+ // --- DETECT NEW FLIGHT/TRANSITION ENTRY ---
if (now_ - last_run_ms > 200) {
align_phase_start_ms = now_;
alignment_done_ms = 0;
last_log_ms = 0;
alignment_completed_for_this_flight = false; // Reset flag for new transition
}
last_run_ms = now_;
// Check Validity & Only run if we haven't finished aligning yet
bool should_run_alignment = !alignment_completed_for_this_flight &&
(plane.control_mode == &plane.mode_auto || plane.control_mode == &plane.mode_guided) &&
(plane.nav_controller != nullptr);
+
+ if (!should_run_alignment) {
+ align_phase_start_ms = 0;
+ alignment_done_ms = 0;
+ } else if (align_phase_start_ms == 0) {
+ align_phase_start_ms = now_;
+ }🤖 Prompt for AI Agents |
||||
| // --- BLOCKING CONTROL LOOP --- | ||||
| // Run this ONLY if we are NOT done waiting AND haven't timed out | ||||
| if (!wait_complete && !timeout_expired) { | ||||
|
|
||||
| // Log (2Hz) | ||||
| if (now_ - last_log_ms > 500) { | ||||
| if (is_aligned) { | ||||
| float remaining = (WAIT_DELAY_MS - (now_ - alignment_done_ms)) * 0.001f; | ||||
| gcs().send_text(MAV_SEVERITY_INFO, "Aligned. Waiting: %.1fs", (double)remaining); | ||||
| } else { | ||||
| gcs().send_text(MAV_SEVERITY_INFO, "Aligning: Err %.1f", abs(error_cd) * 0.01f); | ||||
| } | ||||
| last_log_ms = now_; | ||||
| } | ||||
| //Force zero climb rate (Altitude Hold) | ||||
| quadplane.set_climb_rate_cms(0); | ||||
|
|
||||
| // Control | ||||
| quadplane.pos_control->update_z_controller(); | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are manually calling ardupilot/ArduPlane/quadplane.cpp Line 1040 in a095c6c
Here is a relevant PR from when the comment was added: ArduPilot#19286 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can try calling Will try this when I have time. |
||||
| quadplane.pos_control->update_xy_controller(); | ||||
| quadplane.attitude_control->input_euler_angle_roll_pitch_yaw( | ||||
| quadplane.pos_control->get_roll_cd(), | ||||
| quadplane.pos_control->get_pitch_cd(), | ||||
| (float)target_bearing_cd, | ||||
| false | ||||
| ); | ||||
|
|
||||
| quadplane.motors_output(); | ||||
| set_last_fw_pitch(); // Keep tracking pitch so we don't snap later | ||||
| return; // BLOCK TRANSITION | ||||
| } | ||||
|
|
||||
| // --- HANDOVER LOGIC (Runs ONCE when done) --- | ||||
|
|
||||
| if (timeout_expired) { | ||||
| gcs().send_text(MAV_SEVERITY_WARNING, "Align Timeout: Proceeding"); | ||||
| } else { | ||||
| gcs().send_text(MAV_SEVERITY_INFO, "Alignment Complete: Transitioning"); | ||||
| } | ||||
|
|
||||
| // 1. Mark as complete so we NEVER enter this 'if' block again for this flight | ||||
| alignment_completed_for_this_flight = true; | ||||
|
|
||||
| // 2. Reset Standard Transition Timer to now | ||||
| fw_transition_start_ms = now_; | ||||
|
|
||||
| // 3. Reset Integrators and Pitch Target | ||||
| quadplane.attitude_control->reset_rate_controller_I_terms(); | ||||
| plane.nav_pitch_cd = constrain_float(quadplane.ahrs.pitch_sensor, -8500, 8500); | ||||
| plane.nav_roll_cd = 0; | ||||
| set_last_fw_pitch(); | ||||
|
greptile-apps[bot] marked this conversation as resolved.
|
||||
| } | ||||
| } | ||||
|
|
||||
| // Normal transition code continues here for non-tailsitters | ||||
| // or after tailsitter alignment completes... | ||||
| if (tailsitter.transition_fw_complete()) { | ||||
| // To inform the attitude controller that FW_DONE | ||||
| quadplane.attitude_control->set_tailsitter_transition(false); | ||||
|
|
@@ -1050,8 +1177,6 @@ void Tailsitter_Transition::VTOL_update() | |||
| if (!quadplane.tailsitter.transition_vtol_complete()) { | ||||
| return; | ||||
| } | ||||
| // To inform the attitude controller that VTOL_DONE | ||||
| quadplane.attitude_control->set_tailsitter_back_transition_done(true); | ||||
| // transition to VTOL complete, if armed set vtol rate limit starting point | ||||
| if (plane.arming.is_armed_and_safety_off()) { | ||||
| vtol_limit_start_ms = now; | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -150,14 +150,14 @@ const AP_Param::GroupInfo AC_AttitudeControl::var_info[] = { | |
| // @User: Standard | ||
| AP_GROUPINFO("INPUT_TC", 20, AC_AttitudeControl, _input_tc, AC_ATTITUDE_CONTROL_INPUT_TC_DEFAULT), | ||
|
|
||
| // @Param: RELX_TC | ||
| // @DisplayName: Relaxation time constant | ||
| // @Description: Time constant for relaxing the attitude controller | ||
| // @Units: s | ||
| // @Range: 0.01 10 | ||
| // @Increment: 0.01 | ||
| // @Param: RELX_LO | ||
| // @DisplayName: Relaxation low threshold angle | ||
| // @Description: Pitch angle (degrees) below which the hysteresis deactivates attitude relaxation | ||
| // @Units: deg | ||
| // @Range: 0 90 | ||
| // @Increment: 0.1 | ||
| // @User: Standard | ||
| AP_GROUPINFO("RELX_TC", 21, AC_AttitudeControl, _relax_time_constant, 0.4f), | ||
| AP_GROUPINFO("RELX_LO", 21, AC_AttitudeControl, _low_tilt_relax, 30.0f), | ||
|
|
||
| // @Param: RELX_ANG | ||
| // @DisplayName: Max tilt angle for position controller relaxation | ||
|
|
@@ -166,15 +166,24 @@ const AP_Param::GroupInfo AC_AttitudeControl::var_info[] = { | |
| // @Range: 0 90 | ||
| // @Increment: 0.01 | ||
| // @User: Standard | ||
| AP_GROUPINFO("RELX_ANG", 22, AC_AttitudeControl, _max_tilt_relax, 45.0f), | ||
| AP_GROUPINFO("RELX_HI", 22, AC_AttitudeControl, _high_tilt_relax, 45.0f), | ||
|
|
||
| // @Param: RELX_EN | ||
| // @DisplayName: Position control relaxation enable | ||
| // @Description: Enable/disable flag for postion controller relaxation | ||
| // @Description: Enable/disable flag for position controller relaxation | ||
| // @Values: 0:Disabled, 1:Enabled | ||
| // @User: Advanced | ||
| AP_GROUPINFO("RELX_EN", 23, AC_AttitudeControl, _att_relax_enabled, 0), | ||
|
|
||
| // @Param: RELX_TC | ||
| // @DisplayName: Relaxation time constant | ||
| // @Description: Time constant for the low pass filter on the relaxation factor | ||
| // @Units: s | ||
| // @Range: 0.01 10 | ||
| // @Increment: 0.01 | ||
| // @User: Advanced | ||
| AP_GROUPINFO("RELX_TC", 24, AC_AttitudeControl, _tc_tilt_relax, 1.0f), | ||
|
Comment on lines
150
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| AP_GROUPEND | ||
| }; | ||
|
|
||
|
|
@@ -738,43 +747,32 @@ void AC_AttitudeControl::attitude_controller_run_quat() | |
|
|
||
| // This vector represents the angular error to rotate the thrust vector using x and y and heading using z | ||
| Vector3f attitude_error; | ||
|
|
||
| if(_ts_enabled && _att_relax_enabled){ | ||
| float attitude_tilt; | ||
| compute_tilt_angle(attitude_tilt); | ||
| // Gradually relax roll/pitch setpoint toward zero when tilt exceeds limit | ||
| Vector3f euler; | ||
| _attitude_target.to_euler(euler.x, euler.y, euler.z); | ||
| const float alpha = _dt / (_dt + _relax_time_constant); | ||
| // Gradually bring the setpoint towards zero | ||
| if (fabsf(attitude_tilt) > _max_tilt_relax && !_ts_in_transition) { | ||
| _relaxed_roll *= (1.0f - alpha); | ||
| _relaxed_pitch *= (1.0f - alpha); | ||
| _attitude_target.from_euler(_relaxed_roll, _relaxed_pitch, euler.z); | ||
| _ang_vel_target.x *= (1.0f - alpha); | ||
| _ang_vel_target.y *= (1.0f - alpha); | ||
| static float relaxation_factor_lpf = 0.0f; | ||
| float alpha_relax = _dt / (_dt + _tc_tilt_relax); | ||
| static bool _att_relax_active = false; | ||
| if(_ts_enabled && _att_relax_enabled && !_ts_in_transition){ | ||
| // Linearly relax pitch setpoint toward zero based euler pitch angle. | ||
| Vector3f euler_sp,euler_ang; | ||
| _attitude_target.to_euler(euler_sp.x, euler_sp.y, euler_sp.z); | ||
| attitude_body.to_euler(euler_ang.x, euler_ang.y, euler_ang.z); | ||
| float pitch_tilt = fabsf(degrees(euler_ang.y)); | ||
| if (pitch_tilt > _high_tilt_relax) { | ||
| _att_relax_active = true; | ||
| } else if (pitch_tilt < _low_tilt_relax) { | ||
| _att_relax_active = false; | ||
| } | ||
| // When the attitude is relaxed, gradually recover the setpoint as the vehicle returns within limits | ||
| // 0.5f degree threshold is used to prevent oscillations around the limit when recovering | ||
| else if (fabsf(_relaxed_roll - euler.x) > radians(0.5f) ||fabsf(_relaxed_pitch - euler.y) > radians(0.5f)) { | ||
| // After back transition, set the position control demanded attitude setpoint as the relaxed attitude setpoint. | ||
| if (_ts_back_transition_done) { | ||
| _relaxed_pitch = euler.y; | ||
| _relaxed_roll = euler.x; | ||
| _ts_back_transition_done = false; | ||
| // Run recovery when relaxed attitude is different from current attitude target | ||
| }else{ | ||
| _relaxed_roll += (euler.x - _relaxed_roll) * alpha; | ||
| _relaxed_pitch += (euler.y - _relaxed_pitch) * alpha; | ||
| if (fabsf(_relaxed_roll - euler.x) < radians(0.5f)) _relaxed_roll = euler.x; | ||
| if (fabsf(_relaxed_pitch - euler.y) < radians(0.5f)) _relaxed_pitch = euler.y; | ||
| _attitude_target.from_euler(_relaxed_roll, _relaxed_pitch, euler.z); | ||
| } | ||
| } else { | ||
| // Fully recovered — update the relaxed angles to original setpoint | ||
| _relaxed_roll = euler.x; | ||
| _relaxed_pitch = euler.y; | ||
| //During relaxation pitch setpoint is relaxed towards zero. | ||
| if(_att_relax_active) { | ||
| relaxation_factor_lpf += alpha_relax * (1.0f - relaxation_factor_lpf); | ||
| } | ||
| // When not relaxing, return the setpoint back to the commanded angle. | ||
| else { | ||
| relaxation_factor_lpf += alpha_relax * (0.0f - relaxation_factor_lpf); | ||
| } | ||
| relaxation_factor_lpf = constrain_float(relaxation_factor_lpf, 0.0f, 1.0f); | ||
| euler_sp.y *= (1.0f - relaxation_factor_lpf); | ||
| _ang_vel_target.y *= (1.0f - relaxation_factor_lpf); | ||
| _attitude_target.from_euler(euler_sp.x, euler_sp.y, euler_sp.z); | ||
| } | ||
| thrust_heading_rotation_angles(_attitude_target, attitude_body, attitude_error, _thrust_angle, _thrust_error_angle); | ||
|
|
||
|
|
@@ -792,10 +790,10 @@ void AC_AttitudeControl::attitude_controller_run_quat() | |
|
|
||
| // Correct the thrust vector and smoothly add feedforward and yaw input | ||
| _feedforward_scalar = 1.0f; | ||
| if (_thrust_error_angle > AC_ATTITUDE_THRUST_ERROR_ANGLE * 2.0f) { | ||
| if (_thrust_error_angle > AC_ATTITUDE_THRUST_ERROR_ANGLE * 3.0f) { | ||
| _ang_vel_body.z = _ahrs.get_gyro().z; | ||
| get_rate_yaw_pid().reset_I(); | ||
| } else if (_thrust_error_angle > AC_ATTITUDE_THRUST_ERROR_ANGLE) { | ||
| } else if (_thrust_error_angle > AC_ATTITUDE_THRUST_ERROR_ANGLE * 2.0f) { | ||
| _feedforward_scalar = (1.0f - (_thrust_error_angle - AC_ATTITUDE_THRUST_ERROR_ANGLE) / AC_ATTITUDE_THRUST_ERROR_ANGLE); | ||
| _ang_vel_body.x += ang_vel_body_feedforward.x * _feedforward_scalar; | ||
| _ang_vel_body.y += ang_vel_body_feedforward.y * _feedforward_scalar; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
millis()call and confusingnow_shadow variablenowis already declared at line 973 for the same timestamp. Introducing a secondnow_from a separateAP_HAL::millis()call is a minor timing inconsistency and creates a confusing naming shadow throughout the alignment block. Use the existingnowvariable directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!