trailing whitespace removal (#14026)
[betaflight.git] / src / main / flight / gps_rescue.c
blob461fa6aaf1f62a59fe689e4dae7689c5068b9454
1 /*
2 * This file is part of Betaflight.
4 * Betaflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Betaflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Betaflight. If not, see <http://www.gnu.org/licenses/>.
18 #include <stdint.h>
19 #include <stdlib.h>
20 #include <math.h>
22 #include "platform.h"
24 #ifdef USE_GPS_RESCUE
26 #include "build/debug.h"
28 #include "common/axis.h"
29 #include "common/filter.h"
30 #include "common/maths.h"
31 #include "common/utils.h"
33 #include "config/config.h"
34 #include "drivers/time.h"
36 #include "fc/core.h"
37 #include "fc/rc_controls.h"
38 #include "fc/rc_modes.h"
39 #include "fc/runtime_config.h"
41 #include "flight/autopilot.h"
42 #include "flight/failsafe.h"
43 #include "flight/imu.h"
44 #include "flight/pid.h"
45 #include "flight/position.h"
47 #include "io/gps.h"
48 #include "rx/rx.h"
49 #include "sensors/acceleration.h"
51 #include "gps_rescue.h"
53 typedef enum {
54 RESCUE_IDLE,
55 RESCUE_INITIALIZE,
56 RESCUE_ATTAIN_ALT,
57 RESCUE_ROTATE,
58 RESCUE_FLY_HOME,
59 RESCUE_DESCENT,
60 RESCUE_LANDING,
61 RESCUE_DO_NOTHING,
62 RESCUE_ABORT
63 } rescuePhase_e;
65 typedef enum {
66 RESCUE_HEALTHY,
67 RESCUE_FLYAWAY,
68 RESCUE_GPSLOST,
69 RESCUE_LOWSATS,
70 RESCUE_CRASHFLIP_DETECTED,
71 RESCUE_STALLED,
72 RESCUE_TOO_CLOSE,
73 RESCUE_NO_HOME_POINT
74 } rescueFailureState_e;
76 typedef struct {
77 float maxAltitudeCm;
78 float returnAltitudeCm;
79 float targetAltitudeCm;
80 float targetAltitudeStepCm;
81 float targetVelocityCmS;
82 float pitchAngleLimitDeg;
83 float rollAngleLimitDeg;
84 float descentDistanceM;
85 int8_t secondsFailing;
86 float throttleDMultiplier;
87 float yawAttenuator;
88 float disarmThreshold;
89 float velocityITermAccumulator;
90 float velocityPidCutoff;
91 float velocityPidCutoffModifier;
92 float velocityItermAttenuator;
93 float velocityItermRelax;
94 } rescueIntent_s;
96 typedef struct {
97 float distanceToHomeCm;
98 float distanceToHomeM;
99 uint16_t groundSpeedCmS;
100 int16_t directionToHome;
101 bool healthy;
102 float errorAngle;
103 float gpsDataIntervalSeconds;
104 float velocityToHomeCmS;
105 float alitutudeStepCm;
106 float maxPitchStep;
107 float absErrorAngle;
108 float imuYawCogGain;
109 } rescueSensorData_s;
111 typedef struct {
112 rescuePhase_e phase;
113 rescueFailureState_e failure;
114 rescueSensorData_s sensor;
115 rescueIntent_s intent;
116 bool isAvailable;
117 } rescueState_s;
119 #define GPS_RESCUE_MAX_YAW_RATE 180 // deg/sec max yaw rate
120 #define GPS_RESCUE_MAX_THROTTLE_ITERM 200 // max iterm value for throttle in degrees * 100
121 #define GPS_RESCUE_ALLOWED_YAW_RANGE 30.0f // yaw error must be less than this to enter fly home phase, and to pitch during descend()
123 static const float taskIntervalSeconds = HZ_TO_INTERVAL(TASK_GPS_RESCUE_RATE_HZ); // i.e. 0.01 s
124 static float rescueThrottle;
125 static float rescueYaw;
126 float gpsRescueAngle[ANGLE_INDEX_COUNT] = { 0, 0 };
127 bool magForceDisable = false;
128 static bool newGPSData = false;
129 static pt1Filter_t velocityDLpf;
130 static pt3Filter_t velocityUpsampleLpf;
132 rescueState_s rescueState;
134 void gpsRescueInit(void)
136 float cutoffHz, gain;
137 cutoffHz = gpsRescueConfig()->pitchCutoffHz / 100.0f;
138 rescueState.intent.velocityPidCutoff = cutoffHz;
139 rescueState.intent.velocityPidCutoffModifier = 1.0f;
140 gain = pt1FilterGain(cutoffHz, 1.0f);
141 pt1FilterInit(&velocityDLpf, gain);
143 cutoffHz *= 4.0f;
144 gain = pt3FilterGain(cutoffHz, taskIntervalSeconds);
145 pt3FilterInit(&velocityUpsampleLpf, gain);
149 If we have new GPS data, update home heading if possible and applicable.
151 void gpsRescueNewGpsData(void)
153 newGPSData = true;
156 static void rescueStart(void)
158 rescueState.phase = RESCUE_INITIALIZE;
161 static void rescueStop(void)
163 rescueState.phase = RESCUE_IDLE;
166 // Things that need to run when GPS Rescue is enabled, and while armed, but while there is no Rescue in place
167 static void setReturnAltitude(void)
169 // Hold maxAltitude at zero while disarmed, but if set_home_point_once is true, hold maxAlt until power cycled
170 if (!ARMING_FLAG(ARMED) && !gpsConfig()->gps_set_home_point_once) {
171 rescueState.intent.maxAltitudeCm = 0.0f;
172 return;
175 // While armed, but not during the rescue, update the max altitude value
176 rescueState.intent.maxAltitudeCm = fmaxf(getAltitudeCm(), rescueState.intent.maxAltitudeCm);
178 if (newGPSData) {
179 // set the target altitude to the current altitude.
180 rescueState.intent.targetAltitudeCm = getAltitudeCm();
182 // Intended descent distance for rescues that start outside the minStartDistM distance
183 // Set this to the user's intended descent distance, but not more than half the distance to home to ensure some fly home time
184 rescueState.intent.descentDistanceM = fminf(0.5f * rescueState.sensor.distanceToHomeM, gpsRescueConfig()->descentDistanceM);
186 const float initialClimbCm = gpsRescueConfig()->initialClimbM * 100.0f;
187 switch (gpsRescueConfig()->altitudeMode) {
188 case GPS_RESCUE_ALT_MODE_FIXED:
189 rescueState.intent.returnAltitudeCm = gpsRescueConfig()->returnAltitudeM * 100.0f;
190 break;
191 case GPS_RESCUE_ALT_MODE_CURRENT:
192 // climb above current altitude, but always return at least initial height above takeoff point, in case current altitude was negative
193 rescueState.intent.returnAltitudeCm = fmaxf(initialClimbCm, getAltitudeCm() + initialClimbCm);
194 break;
195 case GPS_RESCUE_ALT_MODE_MAX:
196 default:
197 rescueState.intent.returnAltitudeCm = rescueState.intent.maxAltitudeCm + initialClimbCm;
198 break;
203 static void rescueAttainPosition(void)
205 // runs at 100hz, but only updates RPYT settings when new GPS Data arrives and when not in idle phase.
206 static float previousVelocityError = 0.0f;
207 static float velocityI = 0.0f;
208 switch (rescueState.phase) {
209 case RESCUE_IDLE:
210 // values to be returned when no rescue is active
211 gpsRescueAngle[AI_PITCH] = 0.0f;
212 gpsRescueAngle[AI_ROLL] = 0.0f;
213 rescueThrottle = rcCommand[THROTTLE];
214 return;
215 case RESCUE_INITIALIZE:
216 // Initialize internal variables each time GPS Rescue is started
217 previousVelocityError = 0.0f;
218 velocityI = 0.0f;
219 resetAltitudeControl();
220 rescueState.intent.disarmThreshold = gpsRescueConfig()->disarmThreshold * 0.1f;
221 rescueState.sensor.imuYawCogGain = 1.0f;
222 return;
223 case RESCUE_DO_NOTHING:
224 // 20s of slow descent for switch induced sanity failures to allow time to recover
225 gpsRescueAngle[AI_PITCH] = 0.0f;
226 gpsRescueAngle[AI_ROLL] = 0.0f;
227 rescueThrottle = autopilotConfig()->hover_throttle - 100;
228 return;
229 default:
230 break;
234 Altitude (throttle) controller
237 const float verticalVelocity = getAltitudeDerivative() * rescueState.intent.throttleDMultiplier;
238 altitudeControl(rescueState.intent.targetAltitudeCm, taskIntervalSeconds, verticalVelocity, rescueState.intent.targetAltitudeStepCm);
241 Heading / yaw controller
243 // simple yaw P controller with roll mixed in.
244 // attitude.values.yaw is set by imuCalculateEstimatedAttitude() and is updated from GPS while groundspeed exceeds 2 m/s
245 // below 2m/s groundspeed, the IMU uses gyro to estimate yaw attitude change from previous values
246 // above 2m/s, GPS course over ground us ysed to 'correct' the IMU heading
247 // if the course over ground, due to wind or pre-exiting movement, is different from the attitude of the quad, the GPS correction will be less accurate
248 // the craft should not return much less than 5m/s during the rescue or the GPS corrections may be inaccurate.
249 // the faster the return speed, the more accurate the IMU will be, but the consequences of IMU error at the start are greater
250 // A compass (magnetometer) is vital for accurate GPS rescue at slow speeds, but must be calibrated and validated
251 // WARNING: Some GPS units give false Home values! Always check the arrow points to home on leaving home.
252 rescueYaw = rescueState.sensor.errorAngle * gpsRescueConfig()->yawP * rescueState.intent.yawAttenuator / 10.0f;
253 rescueYaw = constrainf(rescueYaw, -GPS_RESCUE_MAX_YAW_RATE, GPS_RESCUE_MAX_YAW_RATE);
254 // rescueYaw is the yaw rate in deg/s to correct the heading error
256 // *** mix in some roll. very important for heading tracking, since a yaw rate means the quad has drifted sideways
257 const float rollMixAttenuator = constrainf(1.0f - fabsf(rescueYaw) * 0.01f, 0.0f, 1.0f);
258 // less roll at higher yaw rates, no roll at 100 deg/s of yaw
259 const float rollAdjustment = -rescueYaw * gpsRescueConfig()->rollMix * rollMixAttenuator;
260 // if rollMix = 100, the roll:yaw ratio is 1:1 at small angles, reducing linearly to zero when the yaw rate is 100 deg/s
261 // when gpsRescueConfig()->rollMix is zero, there is no roll adjustment
262 // rollAdjustment is degrees * 100
263 // note that the roll element has the opposite sign to the yaw element *before* GET_DIRECTION
264 const float rollLimit = 100.0f * rescueState.intent.rollAngleLimitDeg;
265 gpsRescueAngle[AI_ROLL] = constrainf(rollAdjustment, -rollLimit, rollLimit);
266 // gpsRescueAngle is added to the normal roll Angle Mode corrections in pid.c
268 rescueYaw *= GET_DIRECTION(rcControlsConfig()->yaw_control_reversed);
269 // rescueYaw is the yaw rate in deg/s to correct the heading error
271 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 5, rollMixAttenuator); // 0-1 to suppress roll adjustments at higher yaw rates
272 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 6, gpsRescueAngle[AI_ROLL]); // roll added in degrees*100
273 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 7, rescueYaw); // the yaw rate in deg/s to correct a yaw error
274 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 7, gpsRescueAngle[AI_ROLL]); // roll added in degrees*100
277 Pitch / velocity controller
279 static float pitchAdjustment = 0.0f;
280 if (newGPSData) {
282 const float sampleIntervalNormaliseFactor = rescueState.sensor.gpsDataIntervalSeconds * 10.0f;
284 const float velocityError = rescueState.intent.targetVelocityCmS - rescueState.sensor.velocityToHomeCmS;
285 // velocityError is in cm per second, positive means too slow.
286 // NB positive pitch setpoint means nose down.
287 // target velocity can be very negative leading to large error before the start, with overshoot
289 // P component
290 const float velocityP = velocityError * gpsRescueConfig()->velP;
292 // I component
293 velocityI += 0.01f * gpsRescueConfig()->velI * velocityError * sampleIntervalNormaliseFactor * rescueState.intent.velocityItermRelax;
294 // velocityItermRelax is a time-based factor, 0->1 with time constant of 1s from when we start to fly home
295 // avoids excess iTerm accumulation during the initial acceleration phase and during descent.
297 velocityI *= rescueState.intent.velocityItermAttenuator;
298 // used to minimise iTerm windup during IMU error states and iTerm overshoot in the descent phase
299 // also, if we over-fly the home point, we need to re-accumulate iTerm from zero, not the previously accumulated value
301 const float pitchAngleLimit = rescueState.intent.pitchAngleLimitDeg * 100.0f;
302 const float velocityILimit = 0.5f * pitchAngleLimit;
303 // I component alone cannot exceed half the max pitch angle
304 velocityI = constrainf(velocityI, -velocityILimit, velocityILimit);
306 // D component
307 float velocityD = ((velocityError - previousVelocityError) / sampleIntervalNormaliseFactor);
308 previousVelocityError = velocityError;
309 velocityD *= gpsRescueConfig()->velD;
310 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 5, lrintf(velocityD)); // velocity D before lowpass smoothing
311 // smooth the D steps
312 const float cutoffHz = rescueState.intent.velocityPidCutoff * rescueState.intent.velocityPidCutoffModifier;
313 // note that this cutoff is increased up to 2x as we get closer to landing point in descend()
314 const float gain = pt1FilterGain(cutoffHz, rescueState.sensor.gpsDataIntervalSeconds);
315 pt1FilterUpdateCutoff(&velocityDLpf, gain);
316 velocityD = pt1FilterApply(&velocityDLpf, velocityD);
318 pitchAdjustment = velocityP + velocityI + velocityD;
319 // limit to maximum allowed angle
320 pitchAdjustment = constrainf(pitchAdjustment, -pitchAngleLimit, pitchAngleLimit);
322 // pitchAdjustment is the absolute Pitch angle adjustment value in degrees * 100
323 // it gets added to the normal level mode Pitch adjustments in pid.c
324 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 0, lrintf(velocityP));
325 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 1, lrintf(velocityD));
326 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 4, lrintf(velocityI));
327 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 6, lrintf(rescueState.intent.velocityItermRelax)); // factor attenuates velocity iTerm by multiplication
328 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 7, lrintf(pitchAdjustment)); // rescue pitch angle in degrees * 100
331 // if initiated too close, and in the climb phase, pitch forward in whatever direction the nose is oriented until min distance away
332 // intent is to get far enough away that, with an IMU error, the quad will have enough distance to home to correct that error in the fly home phase
333 if (rescueState.phase == RESCUE_ATTAIN_ALT && rescueState.sensor.distanceToHomeM < gpsRescueConfig()->minStartDistM) {
334 pitchAdjustment = 1500.0f; // 15 degree pitch forward
337 // upsampling and smoothing of pitch angle steps
338 float pitchAdjustmentFiltered = pt3FilterApply(&velocityUpsampleLpf, pitchAdjustment);
340 gpsRescueAngle[AI_PITCH] = pitchAdjustmentFiltered;
341 // this angle gets added to the normal pitch Angle Mode control values in pid.c - will be seen in pitch setpoint
343 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 3, lrintf(rescueState.intent.targetVelocityCmS)); // target velocity to home
344 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 1, lrintf(rescueState.intent.targetVelocityCmS)); // target velocity to home
347 static void performSanityChecks(void)
349 static timeUs_t previousTimeUs = 0; // Last time Stalled/LowSat was checked
350 static float prevAltitudeCm = 0.0f; // to calculate ascent or descent change
351 static float prevTargetAltitudeCm = 0.0f; // to calculate ascent or descent target change
352 static float previousDistanceToHomeCm = 0.0f; // to check that we are returning
353 static int8_t secondsLowSats = 0; // Minimum sat detection
354 static int8_t secondsDoingNothing; // Limit on doing nothing
355 const timeUs_t currentTimeUs = micros();
357 if (rescueState.phase == RESCUE_IDLE) {
358 rescueState.failure = RESCUE_HEALTHY;
359 return;
360 } else if (rescueState.phase == RESCUE_INITIALIZE) {
361 // Initialize these variables each time a GPS Rescue is started
362 previousTimeUs = currentTimeUs;
363 prevAltitudeCm = getAltitudeCm();
364 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
365 previousDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
366 secondsLowSats = 0;
367 secondsDoingNothing = 0;
370 // Handle events that set a failure mode to other than healthy.
371 // Disarm via Abort when sanity on, or for hard Rx loss in FS_ONLY mode
372 // Otherwise allow 20s of semi-controlled descent with impact disarm detection
373 const bool hardFailsafe = !isRxReceivingSignal();
375 if (rescueState.failure != RESCUE_HEALTHY) {
376 // Default to 20s semi-controlled descent with impact detection, then abort
377 rescueState.phase = RESCUE_DO_NOTHING;
379 switch(gpsRescueConfig()->sanityChecks) {
380 case RESCUE_SANITY_ON:
381 rescueState.phase = RESCUE_ABORT;
382 break;
383 case RESCUE_SANITY_FS_ONLY:
384 if (hardFailsafe) {
385 rescueState.phase = RESCUE_ABORT;
387 break;
388 default:
389 // even with sanity checks off,
390 // override when Allow Arming without Fix is enabled without GPS_FIX_HOME and no Control link available.
391 if (gpsRescueConfig()->allowArmingWithoutFix && !STATE(GPS_FIX_HOME) && hardFailsafe) {
392 rescueState.phase = RESCUE_ABORT;
397 // Crash detection is enabled in all rescues. If triggered, immediately disarm.
398 if (crashRecoveryModeActive()) {
399 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
400 disarm(DISARM_REASON_CRASH_PROTECTION);
401 rescueStop();
404 // Check if GPS comms are healthy
405 // ToDo - check if we have an altitude reading; if we have Baro, we can use Landing mode for controlled descent without GPS
406 if (!rescueState.sensor.healthy) {
407 rescueState.failure = RESCUE_GPSLOST;
410 // Things that should run at a low refresh rate (such as flyaway detection, etc) will be checked at 1Hz
411 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
412 if (dTime < 1000000) { //1hz
413 return;
415 previousTimeUs = currentTimeUs;
417 // checks that we are getting closer to home.
418 // if the quad is stuck, or if GPS data packets stop, there will be no change in distance to home
419 // we can't use rescueState.sensor.currentVelocity because it will be held at the last good value if GPS data updates stop
420 if (rescueState.phase == RESCUE_FLY_HOME) {
421 const float velocityToHomeCmS = previousDistanceToHomeCm - rescueState.sensor.distanceToHomeCm; // cm/s
422 previousDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
423 rescueState.intent.secondsFailing += (velocityToHomeCmS < 0.1f * rescueState.intent.targetVelocityCmS) ? 1 : -1;
424 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 30);
425 if (rescueState.intent.secondsFailing >= 30) {
426 #ifdef USE_MAG
427 //If there is a mag and has not been disabled, we have to assume is healthy and has been used in imu.c
428 if (sensors(SENSOR_MAG) && gpsRescueConfig()->useMag && !magForceDisable) {
429 //Try again with mag disabled
430 magForceDisable = true;
431 rescueState.intent.secondsFailing = 0;
432 } else
433 #endif
435 rescueState.failure = RESCUE_FLYAWAY;
440 secondsLowSats += (!STATE(GPS_FIX) || (gpsSol.numSat < GPS_MIN_SAT_COUNT)) ? 1 : -1;
441 secondsLowSats = constrain(secondsLowSats, 0, 10);
443 if (secondsLowSats == 10) {
444 rescueState.failure = RESCUE_LOWSATS;
447 // These conditions ignore sanity mode settings, and apply in all rescues, to handle getting stuck in a climb or descend
449 const float actualAltitudeChange = getAltitudeCm() - prevAltitudeCm;
450 // ** possibly could use getAltitudeDerivative() for for actual altitude change, though it is smoothed
451 const float targetAltitudeChange = rescueState.intent.targetAltitudeCm - prevTargetAltitudeCm;
452 const float ratio = actualAltitudeChange / targetAltitudeChange;
453 prevAltitudeCm = getAltitudeCm();
454 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
456 switch (rescueState.phase) {
457 case RESCUE_LANDING:
458 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
459 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
460 if (rescueState.intent.secondsFailing >= 10) {
461 rescueState.phase = RESCUE_ABORT;
462 // Landing mode shouldn't take more than 10s
464 break;
465 case RESCUE_ATTAIN_ALT:
466 case RESCUE_DESCENT:
467 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
468 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
469 if (rescueState.intent.secondsFailing >= 10) {
470 rescueState.phase = RESCUE_LANDING;
471 rescueState.intent.secondsFailing = 0;
472 // if can't climb, or slow descending, enable impact detection and time out in 10s
474 break;
475 case RESCUE_DO_NOTHING:
476 secondsDoingNothing = MIN(secondsDoingNothing + 1, 20);
477 if (secondsDoingNothing >= 20) {
478 rescueState.phase = RESCUE_ABORT;
479 // time-limited semi-controlled fall with impact detection
481 break;
482 default:
483 // do nothing
484 break;
487 DEBUG_SET(DEBUG_RTH, 2, (rescueState.failure * 10 + rescueState.phase));
488 DEBUG_SET(DEBUG_RTH, 3, (rescueState.intent.secondsFailing * 100 + secondsLowSats));
491 static void sensorUpdate(void)
493 static float prevDistanceToHomeCm = 0.0f;
495 const float altitudeCurrentCm = getAltitudeCm();
496 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 2, lrintf(altitudeCurrentCm));
497 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 0, rescueState.sensor.groundSpeedCmS); // groundspeed cm/s
498 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 1, gpsSol.groundCourse); // degrees * 10
499 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 2, attitude.values.yaw); // degrees * 10
500 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 3, rescueState.sensor.directionToHome); // computed from current GPS position in relation to home
501 rescueState.sensor.healthy = gpsIsHealthy();
503 rescueState.sensor.directionToHome = GPS_directionToHome; // extern value from gps.c using current position relative to home
504 rescueState.sensor.errorAngle = (attitude.values.yaw - rescueState.sensor.directionToHome) / 10.0f;
505 // both attitude and direction are in degrees * 10, errorAngle is degrees
506 if (rescueState.sensor.errorAngle <= -180) {
507 rescueState.sensor.errorAngle += 360;
508 } else if (rescueState.sensor.errorAngle > 180) {
509 rescueState.sensor.errorAngle -= 360;
511 rescueState.sensor.absErrorAngle = fabsf(rescueState.sensor.errorAngle);
513 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 4, lrintf(attitude.values.yaw)); // estimated heading of the quad (direction nose is pointing in)
514 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 5, lrintf(rescueState.sensor.directionToHome)); // angle to home derived from GPS location and home position
516 if (!newGPSData) {
517 return;
518 // GPS ground speed, velocity and distance to home will be held at last good values if no new packets
521 rescueState.sensor.distanceToHomeCm = GPS_distanceToHomeCm;
522 rescueState.sensor.distanceToHomeM = rescueState.sensor.distanceToHomeCm / 100.0f;
523 rescueState.sensor.groundSpeedCmS = gpsSol.groundSpeed; // cm/s
525 rescueState.sensor.gpsDataIntervalSeconds = getGpsDataIntervalSeconds();
526 // Range from 10ms (100hz) to 1000ms (1Hz). Intended to cover common GPS data rates and exclude unusual values.
528 rescueState.sensor.velocityToHomeCmS = ((prevDistanceToHomeCm - rescueState.sensor.distanceToHomeCm) / rescueState.sensor.gpsDataIntervalSeconds);
529 // positive = towards home. First value is useless since prevDistanceToHomeCm was zero.
530 prevDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
532 DEBUG_SET(DEBUG_ATTITUDE, 4, rescueState.sensor.velocityToHomeCmS); // velocity to home
534 // when there is a flyaway due to IMU disorientation, increase IMU yaw CoG gain, and reduce max pitch angle
535 if (gpsRescueConfig()->groundSpeedCmS) {
536 // calculate a factor that can reduce pitch angle when flying away
537 const float rescueGroundspeed = gpsRescueConfig()->imuYawGain * 100.0f; // in cm/s, imuYawGain is m/s groundspeed
538 // rescueGroundspeed is effectively a normalising gain factor for the magnitude of the groundspeed error
539 // a higher value reduces groundspeedErrorRatio, making the radius wider and reducing the circling behaviour
541 const float groundspeedErrorRatio = fabsf(rescueState.sensor.groundSpeedCmS - rescueState.sensor.velocityToHomeCmS) / rescueGroundspeed;
542 // 0 if groundspeed = velocity to home, or both are zero
543 // 1 if forward velocity is zero but sideways speed is imuYawGain in m/s
544 // 2 if moving backwards at imuYawGain m/s, 4 if moving backwards at 2* imuYawGain m/s, etc
546 DEBUG_SET(DEBUG_ATTITUDE, 5, groundspeedErrorRatio * 100);
548 rescueState.intent.velocityItermAttenuator = 4.0f / (groundspeedErrorRatio + 4.0f);
549 // 1 if groundspeedErrorRatio = 0, falling to 2/3 if groundspeedErrorRatio = 2, 0.5 if groundspeedErrorRatio = 4, etc
550 // limit (essentially prevent) velocity iTerm accumulation whenever there is a meaningful groundspeed error
551 // this is a crude but simple way to prevent iTerm windup when recovering from an IMU error
552 // the magnitude of the effect will be less at low GPS data rates, since there will be fewer multiplications per second
553 // but for our purposes this should not matter much, our intent is to severely attenuate iTerm
554 // if, for example, we had a 90 degree attitude error, groundspeedErrorRatio = 1, invGroundspeedError = 0.8,
555 // after 10 iterations, iTerm is 0.1 of what it would have been
556 // also is useful in blocking iTerm accumulation if we overshoot the landing point
558 const float pitchForwardAngle = (gpsRescueAngle[AI_PITCH] > 0.0f) ? fminf(gpsRescueAngle[AI_PITCH] / 3000.0f, 2.0f) : 0.0f;
559 // pitchForwardAngle is positive early in a rescue, and associates with a nose forward ground course
560 // note: gpsRescueAngle[AI_PITCH] is in degrees * 100, and is halved when the IMU is 180 wrong
561 // pitchForwardAngle is 0 when flat
562 // pitchForwardAngle is 0.5 if pitch angle is 15 degrees (ie with rescue angle of 30 and 180deg IMU error)
563 // pitchForwardAngle is 1.0 if pitch angle is 30 degrees (ie with rescue angle of 60 and 180deg IMU error)
564 // pitchForwardAngle is 2.0 if pitch angle is 60 degrees and flying towards home (unlikely to be sustained at that angle)
566 DEBUG_SET(DEBUG_ATTITUDE, 6, pitchForwardAngle * 100.0f);
568 if (rescueState.phase != RESCUE_FLY_HOME) {
569 // prevent IMU disorientation arising from drift during climb, rotate or do nothing phases, which have zero pitch angle
570 // in descent, or too close, increase IMU yaw gain as pitch angle increases
571 rescueState.sensor.imuYawCogGain = pitchForwardAngle;
572 } else {
573 rescueState.sensor.imuYawCogGain = pitchForwardAngle + fminf(groundspeedErrorRatio, 3.5f);
574 // imuYawCogGain will be more positive at higher pitch angles and higher groundspeed errors
575 // imuYawCogGain will be lowest (close to zero) at lower pitch angles and when flying straight towards home
579 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 2, lrintf(rescueState.sensor.velocityToHomeCmS));
580 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 0, lrintf(rescueState.sensor.velocityToHomeCmS));
583 // This function flashes "RESCUE N/A" in the OSD if:
584 // 1. sensor healthy - GPS data is being received.
585 // 2. GPS has a 3D fix.
586 // 3. GPS number of satellites is greater than or equal to the minimum configured satellite count.
587 // Note 1: cannot arm without the required number of sats
588 // hence this flashing indicates that after having enough sats, we now have below the minimum and the rescue likely would fail
589 // Note 2: this function does not take into account the distance from home
590 // The sanity checks are independent, this just provides the OSD warning
591 static bool checkGPSRescueIsAvailable(void)
593 static timeUs_t previousTimeUs = 0; // Last time LowSat was checked
594 const timeUs_t currentTimeUs = micros();
595 static int8_t secondsLowSats = 0; // Minimum sat detection
596 static bool lowsats = false;
597 static bool noGPSfix = false;
598 bool result = true;
600 if (!gpsIsHealthy() || !STATE(GPS_FIX_HOME)) {
601 return false;
604 // Things that should run at a low refresh rate >> ~1hz
605 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
606 if (dTime < 1000000) { //1hz
607 if (noGPSfix || lowsats) {
608 result = false;
610 return result;
613 previousTimeUs = currentTimeUs;
615 if (!STATE(GPS_FIX)) {
616 result = false;
617 noGPSfix = true;
618 } else {
619 noGPSfix = false;
622 secondsLowSats = constrain(secondsLowSats + ((gpsSol.numSat < GPS_MIN_SAT_COUNT) ? 1 : -1), 0, 2);
623 if (secondsLowSats == 2) {
624 lowsats = true;
625 result = false;
626 } else {
627 lowsats = false;
630 return result;
633 void disarmOnImpact(void)
635 if (acc.accMagnitude > rescueState.intent.disarmThreshold) {
636 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
637 disarm(DISARM_REASON_GPS_RESCUE);
638 rescueStop();
642 void descend(void)
644 if (newGPSData) {
645 // consider landing area to be a circle half landing height around home, to avoid overshooting home point
646 const float distanceToLandingAreaM = rescueState.sensor.distanceToHomeM - (0.5f * autopilotConfig()->landing_altitude_m);
647 const float proximityToLandingArea = constrainf(distanceToLandingAreaM / rescueState.intent.descentDistanceM, 0.0f, 1.0f);
649 // increase the velocity lowpass filter cutoff for more aggressive responses when descending, especially close to home
650 // 1.5x when starting descent, 2.5x (smoother) when almost landed
651 rescueState.intent.velocityPidCutoffModifier = 2.5f - proximityToLandingArea;
653 // reduce target velocity as we get closer to home. Zero within 2m of home, reducing risk of overshooting.
654 rescueState.intent.targetVelocityCmS = gpsRescueConfig()->groundSpeedCmS * proximityToLandingArea;
656 // attenuate velocity target unless pointing towards home, to minimise circling behaviour during overshoots
657 if (rescueState.sensor.absErrorAngle > GPS_RESCUE_ALLOWED_YAW_RANGE) {
658 rescueState.intent.targetVelocityCmS = 0;
659 } else {
660 rescueState.intent.targetVelocityCmS *= (GPS_RESCUE_ALLOWED_YAW_RANGE - rescueState.sensor.absErrorAngle) / GPS_RESCUE_ALLOWED_YAW_RANGE;
663 // attenuate velocity iterm towards zero as we get closer to the landing area
664 rescueState.intent.velocityItermAttenuator = fminf(proximityToLandingArea, rescueState.intent.velocityItermAttenuator);
666 // full pitch angle available all the time
667 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->maxRescueAngle;
669 // limit roll angle to half the allowed pitch angle and attenuate when closer to home
670 // keep some roll when at the landing circle distance to avoid endless circling
671 const float proximityToHome = constrainf(rescueState.sensor.distanceToHomeM / rescueState.intent.descentDistanceM, 0.0f, 1.0f);
672 rescueState.intent.rollAngleLimitDeg = 0.5f * rescueState.intent.pitchAngleLimitDeg * proximityToHome;
675 // ensure we have full yaw authority in case we entered descent mode without enough time in fly home to acquire it gracefully
676 rescueState.intent.yawAttenuator = 1.0f;
678 // set the altitude step, considering the interval between altitude readings and the descent rate
679 float altitudeStepCm = taskIntervalSeconds * gpsRescueConfig()->descendRate;
681 // descend more slowly if the return home altitude was less than 20m
682 const float descentRateAttenuator = constrainf (rescueState.intent.returnAltitudeCm / 2000.0f, 0.25f, 1.0f);
683 altitudeStepCm *= descentRateAttenuator;
684 // slowest descent rate will be 1/4 of normal when we start descending at or below 5m above take-off point.
685 // otherwise a rescue initiated very low and close may not get all the way home
687 // descend faster while the quad is at higher altitudes
688 const float descentRateMultiplier = constrainf(rescueState.intent.targetAltitudeCm / 5000.0f, 0.0f, 1.0f);
689 altitudeStepCm *= 1.0f + (2.0f * descentRateMultiplier);
690 // maximum descent rate increase is 3x default above 50m, 2x above 25m, 1.2x at 5m, default by ground level
692 // also increase throttle D up to 2x in the descent phase when altitude descent rate is faster, for better control
693 rescueState.intent.throttleDMultiplier = 1.0f + descentRateMultiplier;
695 rescueState.intent.targetAltitudeStepCm = -altitudeStepCm;
696 rescueState.intent.targetAltitudeCm += rescueState.intent.targetAltitudeStepCm;
699 void initialiseRescueValues (void)
701 rescueState.intent.secondsFailing = 0; // reset the sanity check timer
702 rescueState.intent.yawAttenuator = 0.0f; // no yaw in the climb
703 rescueState.intent.targetVelocityCmS = rescueState.sensor.velocityToHomeCmS; // avoid snap from D at the start
704 rescueState.intent.rollAngleLimitDeg = 0.0f; // no roll until flying home
705 rescueState.intent.throttleDMultiplier = 1.0f;
706 rescueState.intent.velocityPidCutoffModifier = 1.0f; // normal velocity lowpass filter cutoff
707 rescueState.intent.pitchAngleLimitDeg = 0.0f; // force pitch adjustment to zero - level mode will level out
708 rescueState.intent.velocityItermAttenuator = 1.0f; // allow iTerm to accumulate normally unless constrained by IMU error or descent phase
709 rescueState.intent.velocityItermRelax = 0.0f; // but don't accumulate any at the start, not until fly home
710 rescueState.intent.targetAltitudeStepCm = 0.0f;
713 void gpsRescueUpdate(void)
714 // runs at gpsRescueTaskIntervalSeconds, and runs whether or not rescue is active
716 if (!FLIGHT_MODE(GPS_RESCUE_MODE)) {
717 rescueStop(); // sets phase to RESCUE_IDLE; does nothing else. RESCUE_IDLE tasks still run.
718 } else if (FLIGHT_MODE(GPS_RESCUE_MODE) && rescueState.phase == RESCUE_IDLE) {
719 rescueStart(); // sets phase to rescue_initialise if we enter GPS Rescue mode while idle
720 rescueAttainPosition(); // Initialise basic parameters when a Rescue starts (can't initialise sensor data reliably)
721 performSanityChecks(); // Initialises sanity check values when a Rescue starts
724 // Will now be in RESCUE_INITIALIZE mode, if just entered Rescue while IDLE, otherwise stays IDLE
726 sensorUpdate(); // always get latest GPS and Altitude data, update ascend and descend rates
728 static bool returnAltitudeLow = true;
729 static bool initialVelocityLow = true;
730 rescueState.isAvailable = checkGPSRescueIsAvailable();
732 switch (rescueState.phase) {
733 case RESCUE_IDLE:
734 // in Idle phase = NOT in GPS Rescue
735 // update the return altitude and descent distance values, to have valid settings immediately they are needed
736 setReturnAltitude();
737 break;
738 // sanity checks are bypassed in IDLE mode; instead, failure state is always initialised to HEALTHY
739 // target altitude is always set to current altitude.
741 case RESCUE_INITIALIZE:
742 // Things that should be done at the start of a Rescue
743 if (!STATE(GPS_FIX_HOME)) {
744 // we didn't get a home point on arming
745 rescueState.failure = RESCUE_NO_HOME_POINT;
746 // will result in a disarm via the sanity check system, or float around if switch induced
747 } else {
748 if (rescueState.sensor.distanceToHomeM < 5.0f && isBelowLandingAltitude()) {
749 // attempted initiation within 5m of home, and 'on the ground' -> instant disarm, for safety reasons
750 rescueState.phase = RESCUE_ABORT;
751 } else {
752 // attempted initiation within minimum rescue distance requires us to fly out to at least that distance
753 if (rescueState.sensor.distanceToHomeM < gpsRescueConfig()->minStartDistM) {
754 // climb above current height by buffer height, to at least 10m altitude
755 rescueState.intent.returnAltitudeCm = fmaxf(1000.0f, getAltitudeCm() + (gpsRescueConfig()->initialClimbM * 100.0f));
756 // note that the pitch controller will pitch forward to fly out to minStartDistM
757 // set the descent distance to half the minimum rescue distance. which we should have moved out to in the climb phase
758 rescueState.intent.descentDistanceM = 0.5f * gpsRescueConfig()->minStartDistM;
760 // otherwise behave as for a normal rescue
761 initialiseRescueValues ();
762 returnAltitudeLow = getAltitudeCm() < rescueState.intent.returnAltitudeCm;
763 rescueState.phase = RESCUE_ATTAIN_ALT;
766 break;
768 case RESCUE_ATTAIN_ALT:
769 // gradually increment the target altitude until the craft reaches target altitude
770 // note that this can mean the target altitude may increase above returnAltitude if the craft lags target
771 // sanity check will abort if altitude gain is blocked for a cumulative period
772 if (returnAltitudeLow == (getAltitudeCm() < rescueState.intent.returnAltitudeCm)) {
773 // we started low, and still are low; also true if we started high, and still are too high
774 rescueState.intent.targetAltitudeStepCm = (returnAltitudeLow ? gpsRescueConfig()->ascendRate : -1.0f * gpsRescueConfig()->descendRate) * taskIntervalSeconds;
775 rescueState.intent.targetAltitudeCm += rescueState.intent.targetAltitudeStepCm;
777 } else {
778 // target altitude achieved - move on to ROTATE phase, returning at target altitude
779 rescueState.intent.targetAltitudeCm = rescueState.intent.returnAltitudeCm;
780 rescueState.intent.targetAltitudeStepCm = 0.0f;
781 // if initiated too close, do not rotate or do anything else until sufficiently far away that a 'normal' rescue can happen
782 if (rescueState.sensor.distanceToHomeM > gpsRescueConfig()->minStartDistM) {
783 rescueState.phase = RESCUE_ROTATE;
787 // give velocity P and I no error that otherwise could be present due to velocity drift at the start of the rescue
788 rescueState.intent.targetVelocityCmS = rescueState.sensor.velocityToHomeCmS;
789 break;
791 case RESCUE_ROTATE:
792 if (rescueState.intent.yawAttenuator < 1.0f) { // acquire yaw authority over one second
793 rescueState.intent.yawAttenuator += taskIntervalSeconds;
795 if (rescueState.sensor.absErrorAngle < GPS_RESCUE_ALLOWED_YAW_RANGE) {
796 // enter fly home phase, and enable pitch, when the yaw angle error is small enough
797 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->maxRescueAngle;
798 rescueState.phase = RESCUE_FLY_HOME; // enter fly home phase
799 rescueState.intent.secondsFailing = 0; // reset sanity timer for flight home
801 initialVelocityLow = rescueState.sensor.velocityToHomeCmS < gpsRescueConfig()->groundSpeedCmS; // used to set direction of velocity target change
802 rescueState.intent.targetVelocityCmS = rescueState.sensor.velocityToHomeCmS;
803 break;
805 case RESCUE_FLY_HOME:
806 if (rescueState.intent.yawAttenuator < 1.0f) { // be sure to accumulate full yaw authority
807 rescueState.intent.yawAttenuator += taskIntervalSeconds;
809 // velocity PIDs are now active
810 // update target velocity gradually, aiming for rescueGroundspeed with a time constant of 1.0s
811 const float targetVelocityError = gpsRescueConfig()->groundSpeedCmS - rescueState.intent.targetVelocityCmS;
812 const float velocityTargetStep = taskIntervalSeconds * targetVelocityError;
813 // velocityTargetStep is positive when starting low, negative when starting high
814 const bool targetVelocityIsLow = rescueState.intent.targetVelocityCmS < gpsRescueConfig()->groundSpeedCmS;
815 if (initialVelocityLow == targetVelocityIsLow) {
816 // also true if started faster than target velocity and target is still high
817 rescueState.intent.targetVelocityCmS += velocityTargetStep;
820 // slowly introduce velocity iTerm accumulation at start, goes 0 ->1 with time constant 2.0s
821 rescueState.intent.velocityItermRelax += 0.5f * taskIntervalSeconds * (1.0f - rescueState.intent.velocityItermRelax);
822 // there is always a lot of lag at the start, this gradual start avoids excess iTerm accumulation
824 rescueState.intent.velocityPidCutoffModifier = 2.0f - rescueState.intent.velocityItermRelax;
825 // higher velocity filter cutoff for initial few seconds to improve accuracy; can be smoother later
827 if (newGPSData) {
828 // cut back on allowed angle if there is a high groundspeed error
829 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->maxRescueAngle;
830 // introduce roll slowly and limit to half the max pitch angle; earth referenced yaw may add more roll via angle code
831 rescueState.intent.rollAngleLimitDeg = 0.5f * rescueState.intent.pitchAngleLimitDeg * rescueState.intent.velocityItermRelax;
832 if (rescueState.sensor.distanceToHomeM <= rescueState.intent.descentDistanceM) {
833 rescueState.phase = RESCUE_DESCENT;
834 rescueState.intent.secondsFailing = 0; // reset sanity timer for descent
837 break;
839 case RESCUE_DESCENT:
840 // attenuate velocity and altitude targets while updating the heading to home
841 if (isBelowLandingAltitude()) {
842 // enter landing mode once below landing altitude
843 rescueState.phase = RESCUE_LANDING;
844 rescueState.intent.secondsFailing = 0; // reset sanity timer for landing
846 descend();
847 break;
849 case RESCUE_LANDING:
850 // Reduce altitude target steadily until impact, then disarm.
851 // control yaw angle and throttle and pitch, attenuate velocity, roll and pitch iTerm
852 // increase velocity smoothing cutoff as we get closer to ground
853 descend();
854 disarmOnImpact();
855 break;
857 case RESCUE_ABORT:
858 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
859 disarm(DISARM_REASON_FAILSAFE);
860 rescueState.intent.secondsFailing = 0; // reset sanity timers so we can re-arm
861 rescueStop();
862 break;
864 case RESCUE_DO_NOTHING:
865 disarmOnImpact();
866 break;
868 default:
869 break;
872 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 3, lrintf(rescueState.intent.targetAltitudeCm));
873 DEBUG_SET(DEBUG_RTH, 0, lrintf(rescueState.intent.maxAltitudeCm / 10.0f));
875 performSanityChecks();
876 rescueAttainPosition();
878 newGPSData = false;
881 float gpsRescueGetYawRate(void)
883 return rescueYaw;
886 float gpsRescueGetImuYawCogGain(void)
888 return rescueState.sensor.imuYawCogGain;
891 bool gpsRescueIsConfigured(void)
893 return failsafeConfig()->failsafe_procedure == FAILSAFE_PROCEDURE_GPS_RESCUE || isModeActivationConditionPresent(BOXGPSRESCUE);
896 bool gpsRescueIsAvailable(void)
898 return rescueState.isAvailable;
901 bool gpsRescueIsDisabled(void)
902 // used for OSD warning
904 return (!STATE(GPS_FIX_HOME));
907 #ifdef USE_MAG
908 bool gpsRescueDisableMag(void)
910 // Enable mag on user request, but don't use it during fly home or if force disabled
911 // Note that while flying home the course over ground from GPS provides a heading that is less affected by wind
912 return !(gpsRescueConfig()->useMag && rescueState.phase != RESCUE_FLY_HOME && !magForceDisable);
914 #endif
915 #endif