Hadlock

autopilot PD controller for generic flight Sim

Jul 20th, 2025 (edited)
572
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Rust 11.38 KB | Gaming | 0 0
  1. use crate::flight_model::FlightModel;
  2. use crate::geodetic::{bearing_measured_via_great_circle, distance_measured_via_great_circle};
  3. use tracing::{debug, info, error};
  4.  
  5. pub fn update_autopilot(fm: &mut FlightModel, dt: f64) {
  6.     // Handle waypoint navigation first - it may override autopilot targets
  7.     if fm.waypoint_enabled {
  8.         update_waypoint_navigation(fm, dt);
  9.     }
  10.  
  11.     // Only run autopilot if it's enabled
  12.     if !fm.autopilot_enabled {
  13.         return;
  14.     }
  15.  
  16.     // ────────────────────────────────
  17.     // ALTITUDE CONTROL (PD controller)
  18.     // ────────────────────────────────
  19.     const KP_ALT: f64        = 0.08;  // proportional gain (ft → control units)
  20.     const KD_ALT: f64        = 0.04;  // derivative gain (ft/s → control units)
  21.     const ALT_DEADBAND: f64  = 15.0;  // feet
  22.     const MAX_VERT_RATE: f64 = 20.0;  // max climb/descent rate (control units)
  23.  
  24.     let alt_err = fm.target_altitude - fm.airplane_z;
  25.     let climb_cmd = if alt_err.abs() > ALT_DEADBAND {
  26.         let derr = (alt_err - fm.autopilot_prev_alt_error) / dt;
  27.         let u = KP_ALT * alt_err + KD_ALT * derr;
  28.         fm.autopilot_prev_alt_error = alt_err;
  29.         u.clamp(-MAX_VERT_RATE, MAX_VERT_RATE)
  30.     } else {
  31.         0.0    };
  32.     fm.up_down = -climb_cmd;
  33.  
  34.     // ────────────────────────────────
  35.     // HEADING CONTROL ( PD with deadband and gain scheduling)
  36.     // ────────────────────────────────
  37.     const KP_HDG: f64        = 0.6;   // proportional gain (degrees → control units)
  38.     const KD_HDG: f64        = 0.3;   // derivative gain (deg/s → control units)
  39.     const HDG_DEADBAND: f64  = 0.7;   // degrees
  40.     const MAX_ROLL_RATE: f64 = 3.0;   // max roll rate in deg/sec
  41.  
  42.     // Reduce gains when close to target to prevent oscillation
  43.     const FINE_TUNE_THRESHOLD: f64 = 3.0; // degrees - when to start reducing gains
  44.     const DERIVATIVE_FILTER_ALPHA: f64 = 0.8; // low-pass filter for derivative term
  45.  
  46.     let hdg = fm.compass.to_degrees().rem_euclid(360.0);
  47.       // Calculate heading error - use waypoint bearing if in waypoint mode, otherwise use autopilot target
  48.     let hdg_err = if fm.waypoint_enabled {
  49.         // In waypoint mode, use the full error to the waypoint bearing
  50.         (fm.waypoint_target_heading - hdg + 540.0) % 360.0 - 180.0
  51.     } else {
  52.         // Normal autopilot mode - use target heading
  53.         (fm.target_heading - hdg + 540.0) % 360.0 - 180.0
  54.     };    // Always update previous error for derivative term, but filter it to reduce noise
  55.     let raw_derr = (hdg_err - fm.autopilot_prev_hdg_error) / dt;
  56.     let filtered_derr = DERIVATIVE_FILTER_ALPHA * fm.autopilot_filtered_hdg_deriv +
  57.                        (1.0 - DERIVATIVE_FILTER_ALPHA) * raw_derr;
  58.     fm.autopilot_filtered_hdg_deriv = filtered_derr;
  59.     fm.autopilot_prev_hdg_error = hdg_err;      let roll_cmd = if fm.waypoint_enabled {
  60.         // Two-stage control
  61.         // 1. Heading controller determines desired bank angle
  62.         // 2. Bank angle controller executes the bank
  63.  
  64.         let current_bank_deg = fm.side_tilt.to_degrees();
  65.  
  66.         // Stage 1: Heading controller - determine desired bank angle
  67.         let desired_bank_deg = if hdg_err.abs() < HDG_DEADBAND {
  68.             // Close to target heading - level wings
  69.             0.0
  70.         } else {
  71.             // Calculate bank angle for turn
  72.             // Typically 1:1 ratio up to max bank (15-25°)
  73.             let max_bank = 20.0;
  74.             let bank_cmd = hdg_err.clamp(-max_bank, max_bank);
  75.  
  76.             // Reduce bank angle as we approach target to prevent overshoot
  77.             if hdg_err.abs() < 10.0 {
  78.                 bank_cmd * 0.5 // Gentle approach
  79.             } else {
  80.                 bank_cmd
  81.             }
  82.         };
  83.  
  84.         // Stage 2: Bank angle controller - execute the bank
  85.         let bank_error = desired_bank_deg - current_bank_deg;
  86.  
  87.         // Simple proportional controller for bank angle
  88.         // Uses high gain for bank control (typically 2-5)
  89.         let bank_gain = 3.0;
  90.         let roll_cmd = bank_error * bank_gain;
  91.  
  92.         // Limit roll rate
  93.         roll_cmd.clamp(-MAX_ROLL_RATE, MAX_ROLL_RATE)
  94.     } else {
  95.         // Normal autopilot mode with improved anti-oscillation
  96.         if hdg_err.abs() > HDG_DEADBAND {
  97.             // Apply gain scheduling: reduce gains when close to target
  98.             let gain_factor = if hdg_err.abs() < FINE_TUNE_THRESHOLD {
  99.                 // Reduce gains when within fine-tune threshold to prevent oscillation
  100.                 0.3 + 0.7 * (hdg_err.abs() / FINE_TUNE_THRESHOLD)
  101.             } else {
  102.                 1.0
  103.             };
  104.  
  105.             let kp_adjusted = KP_HDG * gain_factor;
  106.             let kd_adjusted = KD_HDG * gain_factor;
  107.  
  108.             let u = kp_adjusted * hdg_err + kd_adjusted * filtered_derr;
  109.             u.clamp(-MAX_ROLL_RATE, MAX_ROLL_RATE)
  110.         } else {
  111.             0.0
  112.         }
  113.     };// Debug output for waypoint mode
  114.     if fm.waypoint_enabled {
  115.         let current_bank_deg = fm.side_tilt.to_degrees();
  116.  
  117.         // Calculate what the desired bank should be for debug
  118.         let desired_bank_deg = if hdg_err.abs() < HDG_DEADBAND {
  119.             0.0
  120.         } else {
  121.             let max_bank = 20.0;
  122.             let bank_cmd = hdg_err.clamp(-max_bank, max_bank);
  123.  
  124.             if hdg_err.abs() < 10.0 {
  125.                 bank_cmd * 0.5
  126.             } else {
  127.                 bank_cmd
  128.             }
  129.         };
  130.  
  131.         let bank_error = desired_bank_deg - current_bank_deg;
  132.  
  133.         let status = if hdg_err.abs() < HDG_DEADBAND {
  134.             "ON_HEADING"
  135.         } else if bank_error.abs() > 5.0 {
  136.             "BANKING"
  137.         } else {
  138.             "FINE_TUNING"
  139.         };
  140.           debug!("Autopilot: hdg_err={:.1}°, roll_cmd={:.2}, current_bank={:.1}°, desired_bank={:.1}°, bank_err={:.1}°, status={}",
  141.                  hdg_err, roll_cmd, current_bank_deg, desired_bank_deg, bank_error, status);
  142.     }
  143.  
  144.     fm.left_right = -roll_cmd;
  145. }
  146.  
  147. fn update_waypoint_navigation(fm: &mut FlightModel, _dt: f64) {
  148.     // Update waypoint logic
  149.     update_waypoint_navigation_internal(fm);
  150.  
  151.     if !fm.waypoint_enabled || fm.waypoints.is_empty() {
  152.         return;
  153.     }
  154.  
  155.     // Ensure autopilot is enabled for waypoint navigation
  156.     if !fm.autopilot_enabled {
  157.         fm.autopilot_enabled = true;
  158.     }    // Update targets for current waypoint
  159.     if let Some(waypoint) = fm.waypoints.get(fm.current_waypoint_index) {
  160.         // Set altitude target
  161.         fm.target_altitude = waypoint.alt;
  162.  
  163.         // Calculate desired bearing to waypoint using great circle
  164.         let desired_bearing_deg = bearing_measured_via_great_circle(
  165.             fm.current_lat, fm.current_lon,
  166.             waypoint.lat, waypoint.lon
  167.         );
  168.  
  169.         // Store the actual waypoint bearing for HUD display and autopilot control
  170.         fm.waypoint_target_heading = desired_bearing_deg;
  171.  
  172.         debug!("Waypoint bearing: {:.0}° (from {:.6},{:.6} to {:.6},{:.6})",
  173.                  desired_bearing_deg, fm.current_lat, fm.current_lon, waypoint.lat, waypoint.lon);
  174.     }
  175. }
  176.  
  177. // Autopilot control
  178. pub fn toggle_autopilot(fm: &mut FlightModel) {
  179.     fm.autopilot_enabled = !fm.autopilot_enabled;
  180.     if fm.autopilot_enabled {
  181.         // Capture current altitude and heading when enabling autopilot
  182.         fm.target_altitude = fm.airplane_z;
  183.         // Convert compass radians to degrees for easier handling
  184.         fm.target_heading = (fm.compass * 57.2957795131).rem_euclid(360.0);
  185.         info!("Autopilot ENABLED - Target altitude: {:.0} ft, Target heading: {:.0}°",
  186.                     fm.target_altitude, fm.target_heading);
  187.     } else {
  188.         info!("Autopilot DISABLED");
  189.     }
  190. }
  191.  
  192. // Waypoint navigation control
  193. pub fn toggle_waypoint(fm: &mut FlightModel) {
  194.     fm.waypoint_enabled = !fm.waypoint_enabled;
  195.     if fm.waypoint_enabled {
  196.         if !fm.waypoints.is_empty() {
  197.             // Reset to first waypoint when enabling
  198.             fm.current_waypoint_index = 0;
  199.             navigate_to_current_waypoint(fm);
  200.             info!("Waypoint navigation ENABLED - Going to waypoint 1 of {}", fm.waypoints.len());
  201.         } else {
  202.             fm.waypoint_enabled = false;
  203.             info!("No waypoints loaded - waypoint navigation disabled");
  204.         }
  205.     } else {
  206.         // When disabling waypoint navigation, reset autopilot to current heading/altitude
  207.         if fm.autopilot_enabled {
  208.             fm.target_altitude = fm.airplane_z;
  209.             fm.target_heading = (fm.compass * 57.2957795131).rem_euclid(360.0);
  210.             info!("Waypoint navigation DISABLED - Autopilot set to hold current heading {:.0}° and altitude {:.0}ft",
  211.                         fm.target_heading, fm.target_altitude);
  212.         } else {
  213.             info!("Waypoint navigation DISABLED");
  214.         }
  215.     }
  216. }
  217. pub fn set_waypoints(fm: &mut FlightModel, waypoints: Vec<crate::config::Waypoint>) {
  218.     fm.waypoints = waypoints;
  219.     fm.current_waypoint_index = 0;
  220.     info!("Loaded {} waypoints", fm.waypoints.len());
  221. }
  222.  
  223. fn navigate_to_current_waypoint(fm: &mut FlightModel) {
  224.     if let Some(waypoint) = fm.waypoints.get(fm.current_waypoint_index) {
  225.         debug!("Navigating to waypoint {} at ({:.6}°, {:.6}°, {:.0} ft)",
  226.                     fm.current_waypoint_index + 1, waypoint.lat, waypoint.lon, waypoint.alt);
  227.     }
  228. }
  229.  
  230. pub fn update_waypoint_navigation_internal(fm: &mut FlightModel) {
  231.     if !fm.waypoint_enabled || fm.waypoints.is_empty() {
  232.         return;
  233.     }
  234.  
  235.     if let Some(current_waypoint) = fm.waypoints.get(fm.current_waypoint_index) {
  236.         // Check if we're within waypoint threshold using great circle distance
  237.         let distance_nm = distance_measured_via_great_circle(
  238.             fm.current_lat, fm.current_lon,
  239.             current_waypoint.lat, current_waypoint.lon
  240.         );
  241.         let altitude_diff = (current_waypoint.alt - fm.airplane_z).abs();
  242.             // Waypoint reached if within 0.2 nm horizontally and 300 ft vertically
  243.         if distance_nm <= 0.2 && altitude_diff <= 300.0 {
  244.             error!("Reached waypoint {} at ({:.6}°, {:.6}°, {:.0} ft)",
  245.                         fm.current_waypoint_index + 1, current_waypoint.lat, current_waypoint.lon, current_waypoint.alt);
  246.  
  247.             // Move to next waypoint
  248.             fm.current_waypoint_index += 1;
  249.  
  250.             if fm.current_waypoint_index >= fm.waypoints.len() {
  251.                 // All waypoints completed
  252.                 error!("All waypoints completed! Disabling waypoint navigation.");
  253.                 fm.waypoint_enabled = false;
  254.             } else {
  255.                 // Navigate to next waypoint
  256.                 navigate_to_current_waypoint(fm);
  257.             }
  258.         }
  259.     }
  260. }
  261.  
  262. /// Calculate distance to current waypoint in nautical miles
  263. pub fn distance_to_current_waypoint(fm: &FlightModel) -> Option<f64> {
  264.     if !fm.waypoint_enabled || fm.current_waypoint_index >= fm.waypoints.len() {
  265.         return None;
  266.     }
  267.  
  268.     let waypoint = &fm.waypoints[fm.current_waypoint_index];
  269.  
  270.     // Use great circle distance since waypoints are now in lat/lon
  271.     Some(distance_measured_via_great_circle(
  272.         fm.current_lat, fm.current_lon,
  273.         waypoint.lat, waypoint.lon
  274.     ))
  275. }
Advertisement
Add Comment
Please, Sign In to add comment