Dedicated task for GPS Rescue (#11972)
[betaflight.git] / src / main / flight / gps_rescue.c
blobc95924f60087b31779f3350d6ba7d91b476b40bf
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/maths.h"
30 #include "common/utils.h"
32 #include "drivers/time.h"
34 #include "io/gps.h"
36 #include "config/config.h"
37 #include "fc/core.h"
38 #include "fc/rc_controls.h"
39 #include "fc/rc_modes.h"
40 #include "fc/runtime_config.h"
42 #include "flight/failsafe.h"
43 #include "flight/imu.h"
44 #include "flight/pid.h"
45 #include "flight/position.h"
47 #include "pg/pg.h"
48 #include "pg/pg_ids.h"
50 #include "rx/rx.h"
52 #include "sensors/acceleration.h"
54 #include "gps_rescue.h"
56 typedef enum {
57 RESCUE_SANITY_OFF = 0,
58 RESCUE_SANITY_ON,
59 RESCUE_SANITY_FS_ONLY
60 } gpsRescueSanity_e;
62 typedef enum {
63 RESCUE_IDLE,
64 RESCUE_INITIALIZE,
65 RESCUE_ATTAIN_ALT,
66 RESCUE_ROTATE,
67 RESCUE_FLY_HOME,
68 RESCUE_DESCENT,
69 RESCUE_LANDING,
70 RESCUE_ABORT,
71 RESCUE_COMPLETE,
72 RESCUE_DO_NOTHING
73 } rescuePhase_e;
75 typedef enum {
76 RESCUE_HEALTHY,
77 RESCUE_FLYAWAY,
78 RESCUE_GPSLOST,
79 RESCUE_LOWSATS,
80 RESCUE_CRASH_FLIP_DETECTED,
81 RESCUE_STALLED,
82 RESCUE_TOO_CLOSE,
83 RESCUE_NO_HOME_POINT
84 } rescueFailureState_e;
86 typedef struct {
87 float maxAltitudeCm;
88 float returnAltitudeCm;
89 float targetAltitudeCm;
90 float targetLandingAltitudeCm;
91 float targetVelocityCmS;
92 float pitchAngleLimitDeg;
93 float rollAngleLimitDeg;
94 float descentDistanceM;
95 int8_t secondsFailing;
96 float altitudeStep;
97 float descentRateModifier;
98 float yawAttenuator;
99 float disarmThreshold;
100 } rescueIntent_s;
102 typedef struct {
103 float currentAltitudeCm;
104 float distanceToHomeCm;
105 float distanceToHomeM;
106 uint16_t groundSpeedCmS;
107 int16_t directionToHome;
108 float accMagnitude;
109 bool healthy;
110 float errorAngle;
111 float gpsDataIntervalSeconds;
112 float altitudeDataIntervalSeconds;
113 float velocityToHomeCmS;
114 float alitutudeStepCm;
115 float maxPitchStep;
116 float filterK;
117 float absErrorAngle;
118 } rescueSensorData_s;
120 typedef struct {
121 rescuePhase_e phase;
122 rescueFailureState_e failure;
123 rescueSensorData_s sensor;
124 rescueIntent_s intent;
125 bool isAvailable;
126 } rescueState_s;
128 typedef enum {
129 MAX_ALT,
130 FIXED_ALT,
131 CURRENT_ALT
132 } altitudeMode_e;
134 #define GPS_RESCUE_MAX_YAW_RATE 180 // deg/sec max yaw rate
135 #define GPS_RESCUE_MIN_DESCENT_DIST_M 5 // minimum descent distance
136 #define GPS_RESCUE_MAX_ITERM_VELOCITY 1000 // max iterm value for velocity
137 #define GPS_RESCUE_MAX_ITERM_THROTTLE 200 // max iterm value for throttle
138 #define GPS_RESCUE_MAX_PITCH_RATE 3000 // max change in pitch per second in degrees * 100
139 #define GPS_RESCUE_DISARM_THRESHOLD 2.0f // disarm threshold in G's
141 #ifdef USE_MAG
142 #define GPS_RESCUE_USE_MAG true
143 #else
144 #define GPS_RESCUE_USE_MAG false
145 #endif
147 PG_REGISTER_WITH_RESET_TEMPLATE(gpsRescueConfig_t, gpsRescueConfig, PG_GPS_RESCUE, 3);
149 PG_RESET_TEMPLATE(gpsRescueConfig_t, gpsRescueConfig,
150 .minRescueDth = 30,
151 .altitudeMode = MAX_ALT,
152 .rescueAltitudeBufferM = 10,
153 .ascendRate = 500, // cm/s, for altitude corrections on ascent
155 .initialAltitudeM = 30,
156 .rescueGroundspeed = 500,
157 .angle = 40,
158 .rollMix = 150,
160 .descentDistanceM = 20,
161 .descendRate = 100, // cm/s, minimum for descent and landing phase, or for descending if starting high ascent
162 .targetLandingAltitudeM = 4,
164 .throttleMin = 1100,
165 .throttleMax = 1600,
166 .throttleHover = 1275,
168 .allowArmingWithoutFix = false,
169 .sanityChecks = RESCUE_SANITY_FS_ONLY,
170 .minSats = 8,
172 .throttleP = 15,
173 .throttleI = 15,
174 .throttleD = 15,
175 .velP = 8,
176 .velI = 30,
177 .velD = 20,
178 .yawP = 20,
180 .useMag = GPS_RESCUE_USE_MAG
183 static float rescueThrottle;
184 static float rescueYaw;
185 float gpsRescueAngle[ANGLE_INDEX_COUNT] = { 0, 0 };
186 bool magForceDisable = false;
187 static bool newGPSData = false;
188 static pt2Filter_t throttleDLpf;
189 static pt3Filter_t pitchLpf;
191 rescueState_s rescueState;
193 void gpsRescueInit(void)
195 const float sampleTimeS = HZ_TO_INTERVAL(TASK_ALTITUDE_RATE_HZ);
197 const float throttleDCutoffHz = positionConfig()->altitude_d_lpf / 100.0f;
198 const float throttleDCutoffGain = pt2FilterGain(throttleDCutoffHz, sampleTimeS);
199 pt2FilterInit(&throttleDLpf, throttleDCutoffGain);
201 const float pitchCutoffHz = 4.0f;
202 const float pitchCutoffGain = pt3FilterGain(pitchCutoffHz, sampleTimeS);
203 pt3FilterInit(&pitchLpf, pitchCutoffGain);
207 If we have new GPS data, update home heading if possible and applicable.
209 void gpsRescueNewGpsData(void)
211 newGPSData = true;
214 static void rescueStart(void)
216 rescueState.phase = RESCUE_INITIALIZE;
219 static void rescueStop(void)
221 rescueState.phase = RESCUE_IDLE;
224 // Things that need to run when GPS Rescue is enabled, and while armed, but while there is no Rescue in place
225 static void setReturnAltitude(void)
227 // Hold maxAltitude at zero while disarmed, but if set_home_point_once is true, hold maxAlt until power cycled
228 if (!ARMING_FLAG(ARMED) && !gpsConfig()->gps_set_home_point_once) {
229 rescueState.intent.maxAltitudeCm = 0.0f;
230 return;
233 // While armed, but not during the rescue, update the max altitude value
234 rescueState.intent.maxAltitudeCm = fmaxf(rescueState.sensor.currentAltitudeCm, rescueState.intent.maxAltitudeCm);
236 if (newGPSData) {
237 // set the target altitude to current values, so there will be no D kick on first run
238 rescueState.intent.targetAltitudeCm = rescueState.sensor.currentAltitudeCm;
240 // Keep the descent distance and intended altitude up to date with latest GPS values
241 rescueState.intent.descentDistanceM = constrainf(rescueState.sensor.distanceToHomeM, GPS_RESCUE_MIN_DESCENT_DIST_M, gpsRescueConfig()->descentDistanceM);
242 const float initialAltitudeCm = gpsRescueConfig()->initialAltitudeM * 100.0f;
243 const float rescueAltitudeBufferCm = gpsRescueConfig()->rescueAltitudeBufferM * 100.0f;
244 switch (gpsRescueConfig()->altitudeMode) {
245 case FIXED_ALT:
246 rescueState.intent.returnAltitudeCm = initialAltitudeCm;
247 break;
248 case CURRENT_ALT:
249 rescueState.intent.returnAltitudeCm = rescueState.sensor.currentAltitudeCm + rescueAltitudeBufferCm;
250 break;
251 case MAX_ALT:
252 default:
253 rescueState.intent.returnAltitudeCm = rescueState.intent.maxAltitudeCm + rescueAltitudeBufferCm;
254 break;
259 static void rescueAttainPosition(void)
261 // runs at 100hz, but only updates RPYT settings when new GPS Data arrives and when not in idle phase.
262 static float previousVelocityError = 0.0f;
263 static float velocityI = 0.0f;
264 static float previousVelocityD = 0.0f; // for smoothing
265 static float previousPitchAdjustment = 0.0f;
266 static float throttleI = 0.0f;
267 static float previousAltitudeError = 0.0f;
268 static int16_t throttleAdjustment = 0;
270 switch (rescueState.phase) {
271 case RESCUE_IDLE:
272 // values to be returned when no rescue is active
273 gpsRescueAngle[AI_PITCH] = 0.0f;
274 gpsRescueAngle[AI_ROLL] = 0.0f;
275 rescueThrottle = rcCommand[THROTTLE];
276 return;
277 case RESCUE_INITIALIZE:
278 // Initialize internal variables each time GPS Rescue is started
279 previousVelocityError = 0.0f;
280 velocityI = 0.0f;
281 previousVelocityD = 0.0f;
282 previousPitchAdjustment = 0.0f;
283 throttleI = 0.0f;
284 previousAltitudeError = 0.0f;
285 throttleAdjustment = 0;
286 rescueState.intent.disarmThreshold = GPS_RESCUE_DISARM_THRESHOLD;
287 return;
288 case RESCUE_DO_NOTHING:
289 // 20s of slow descent for switch induced sanity failures to allow time to recover
290 gpsRescueAngle[AI_PITCH] = 0.0f;
291 gpsRescueAngle[AI_ROLL] = 0.0f;
292 rescueThrottle = gpsRescueConfig()->throttleHover - 100;
293 return;
294 default:
295 break;
299 Altitude (throttle) controller
301 // currentAltitudeCm is updated at TASK_GPS_RESCUE_RATE_HZ
302 const float altitudeError = (rescueState.intent.targetAltitudeCm - rescueState.sensor.currentAltitudeCm) * 0.01f;
303 // height above target in metres (negative means too low)
304 // at the start, the target starts at current altitude plus one step. Increases stepwise to intended value.
306 // P component
307 const float throttleP = gpsRescueConfig()->throttleP * altitudeError;
309 // I component
310 throttleI += 0.1f * gpsRescueConfig()->throttleI * altitudeError * rescueState.sensor.altitudeDataIntervalSeconds;
311 throttleI = constrainf(throttleI, -1.0f * GPS_RESCUE_MAX_ITERM_THROTTLE, 1.0f * GPS_RESCUE_MAX_ITERM_THROTTLE);
312 // up to 20% increase in throttle from I alone
314 // D component is error based, so includes positive boost when climbing and negative boost on descent
315 float verticalSpeed = ((altitudeError - previousAltitudeError) / rescueState.sensor.altitudeDataIntervalSeconds);
316 previousAltitudeError = altitudeError;
317 verticalSpeed += rescueState.intent.descentRateModifier * verticalSpeed;
318 // add up to 2x D when descent rate is faster
320 float throttleD = pt2FilterApply(&throttleDLpf, verticalSpeed);
322 rescueState.intent.disarmThreshold = GPS_RESCUE_DISARM_THRESHOLD - throttleD / 15.0f; // make disarm more likely if throttle D is high
324 throttleD = gpsRescueConfig()->throttleD * throttleD;
326 // acceleration component not currently implemented - was needed previously due to GPS lag, maybe not needed now.
328 float tiltAdjustment = 1.0f - getCosTiltAngle(); // 0 = flat, gets to 0.2 correcting on a windy day
329 tiltAdjustment *= (gpsRescueConfig()->throttleHover - 1000);
330 // if hover is 1300, and adjustment .2, this gives us 0.2*300 or 60 of extra throttle, not much, but useful
331 // too much and landings with lots of pitch adjustment, eg windy days, can be a problem
333 throttleAdjustment = throttleP + throttleI + throttleD + tiltAdjustment;
335 rescueThrottle = gpsRescueConfig()->throttleHover + throttleAdjustment;
336 rescueThrottle = constrainf(rescueThrottle, gpsRescueConfig()->throttleMin, gpsRescueConfig()->throttleMax);
337 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 0, lrintf(throttleP));
338 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 1, lrintf(throttleD));
341 Heading / yaw controller
343 // simple yaw P controller with roll mixed in.
344 // attitude.values.yaw is set by imuCalculateEstimatedAttitude() and is updated from GPS while groundspeed exceeds 2 m/s
345 // below 2m/s groundspeed, the IMU uses gyro to estimate yaw attitude change from previous values
346 // above 2m/s, GPS course over ground us ysed to 'correct' the IMU heading
347 // 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
348 // the craft should not return much less than 5m/s during the rescue or the GPS corrections may be inaccurate.
349 // the faster the return speed, the more accurate the IMU will be, but the consequences of IMU error at the start are greater
350 // A compass (magnetometer) is vital for accurate GPS rescue at slow speeds, but must be calibrated and validated.
352 rescueYaw = rescueState.sensor.errorAngle * gpsRescueConfig()->yawP * rescueState.intent.yawAttenuator * 0.1f;
353 rescueYaw = constrainf(rescueYaw, -GPS_RESCUE_MAX_YAW_RATE, GPS_RESCUE_MAX_YAW_RATE);
354 // rescueYaw is the yaw rate in deg/s to correct the heading error
356 const float rollMixAttenuator = constrainf(1.0f - fabsf(rescueYaw) * 0.01f, 0.0f, 1.0f);
357 // less roll at higher yaw rates, no roll at 100 deg/s of yaw
358 const float rollAdjustment = -rescueYaw * gpsRescueConfig()->rollMix * rollMixAttenuator;
359 // 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
360 // when gpsRescueConfig()->rollMix is zero, there is no roll adjustment
361 // rollAdjustment is degrees * 100
362 // note that the roll element has the opposite sign to the yaw element *before* GET_DIRECTION
364 const float rollLimit = 100.0f * rescueState.intent.rollAngleLimitDeg;
365 gpsRescueAngle[AI_ROLL] = constrainf(rollAdjustment, -rollLimit, rollLimit);
366 // gpsRescueAngle is added to the normal roll Angle Mode corrections in pid.c
368 rescueYaw *= GET_DIRECTION(rcControlsConfig()->yaw_control_reversed);
371 Pitch / velocity controller
373 static float pitchAdjustment = 0.0f;
374 if (newGPSData) {
376 const float sampleIntervalNormaliseFactor = rescueState.sensor.gpsDataIntervalSeconds * 10.0f;
378 const float velocityError = (rescueState.intent.targetVelocityCmS - rescueState.sensor.velocityToHomeCmS);
379 // velocityError is in cm per second, positive means too slow.
380 // NB positive pitch setpoint means nose down.
382 // P component
383 const float velocityP = velocityError * gpsRescueConfig()->velP;
385 // I component
386 velocityI += 0.01f * gpsRescueConfig()->velI * velocityError * sampleIntervalNormaliseFactor;
387 // increase amount added when GPS sample rate is slower
388 velocityI = constrainf(velocityI, -1.0f * GPS_RESCUE_MAX_ITERM_VELOCITY, 1.0f * GPS_RESCUE_MAX_ITERM_VELOCITY);
389 // I component alone cannot exceed a pitch angle of 10%
391 // D component
392 float velocityD = ((velocityError - previousVelocityError) / sampleIntervalNormaliseFactor);
393 previousVelocityError = velocityError;
394 // simple first order filter on derivative with k = 0.5 for 200ms steps
395 velocityD = previousVelocityD + rescueState.sensor.filterK * (velocityD - previousVelocityD);
396 previousVelocityD = velocityD;
397 velocityD *= gpsRescueConfig()->velD;
399 const float velocityIAttenuator = rescueState.intent.targetVelocityCmS / gpsRescueConfig()->rescueGroundspeed;
400 // reduces iTerm as target velocity decreases, to minimise overshoot during deceleration to landing phase
402 pitchAdjustment = velocityP + velocityD;
403 if (rescueState.phase == RESCUE_FLY_HOME) {
404 pitchAdjustment *= 0.7f; // attenuate pitch PIDs during main fly home phase, tighten up in descent.
406 pitchAdjustment += velocityI * velocityIAttenuator;
408 const float movingAvgPitchAdjustment = 0.5f * (previousPitchAdjustment + pitchAdjustment);
409 // moving average seems to work best here, a lot of sequential up and down in velocity data
410 previousPitchAdjustment = pitchAdjustment;
411 pitchAdjustment = movingAvgPitchAdjustment;
412 // pitchAdjustment is the absolute Pitch angle adjustment value in degrees * 100
413 // it gets added to the normal level mode Pitch adjustments in pid.c
414 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 0, lrintf(velocityP));
415 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 1, lrintf(velocityD));
418 const float pitchAdjustmentFiltered = pt3FilterApply(&pitchLpf, pitchAdjustment);
419 // upsampling and smoothing of pitch angle steps
421 const float pitchAngleLimit = rescueState.intent.pitchAngleLimitDeg * 100.0f;
422 gpsRescueAngle[AI_PITCH] = constrainf(pitchAdjustmentFiltered, -pitchAngleLimit, pitchAngleLimit);
423 // this angle gets added to the normal pitch Angle Mode control values in pid.c - will be seen in pitch setpoint
425 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 3, lrintf(rescueState.intent.targetVelocityCmS));
426 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 1, lrintf(rescueState.intent.targetVelocityCmS));
429 static void performSanityChecks(void)
431 static timeUs_t previousTimeUs = 0; // Last time Stalled/LowSat was checked
432 static float prevAltitudeCm = 0.0f; // to calculate ascent or descent change
433 static float prevTargetAltitudeCm = 0.0f; // to calculate ascent or descent target change
434 static float previousDistanceToHomeCm = 0.0f; // to check that we are returning
435 static int8_t secondsLowSats = 0; // Minimum sat detection
436 static int8_t secondsDoingNothing = 0; // Limit on doing nothing
437 const timeUs_t currentTimeUs = micros();
439 if (rescueState.phase == RESCUE_IDLE) {
440 rescueState.failure = RESCUE_HEALTHY;
441 return;
442 } else if (rescueState.phase == RESCUE_INITIALIZE) {
443 // Initialize these variables each time a GPS Rescue is started
444 previousTimeUs = currentTimeUs;
445 prevAltitudeCm = rescueState.sensor.currentAltitudeCm;
446 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
447 previousDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
448 secondsLowSats = 0;
449 secondsDoingNothing = 0;
450 return;
453 // Handle events that set a failure mode to other than healthy.
454 // Disarm via Abort when sanity on, or for hard Rx loss in FS_ONLY mode
455 // Otherwise allow 20s of semi-controlled descent with impact disarm detection
456 const bool hardFailsafe = !rxIsReceivingSignal();
457 if (rescueState.failure != RESCUE_HEALTHY) {
458 if (gpsRescueConfig()->sanityChecks == RESCUE_SANITY_ON) {
459 rescueState.phase = RESCUE_ABORT;
460 } else if ((gpsRescueConfig()->sanityChecks == RESCUE_SANITY_FS_ONLY) && hardFailsafe) {
461 rescueState.phase = RESCUE_ABORT;
462 } else {
463 // even with sanity checks off,
464 rescueState.phase = RESCUE_DO_NOTHING; // 20s semi-controlled descent with impact detection, then abort
468 // Crash detection is enabled in all rescues. If triggered, immediately disarm.
469 if (crashRecoveryModeActive()) {
470 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
471 disarm(DISARM_REASON_CRASH_PROTECTION);
472 rescueStop();
475 // Check if GPS comms are healthy
476 // ToDo - check if we have an altitude reading; if we have Baro, we can use Landing mode for controlled descent without GPS
477 if (!rescueState.sensor.healthy) {
478 rescueState.failure = RESCUE_GPSLOST;
481 // Things that should run at a low refresh rate (such as flyaway detection, etc) will be checked at 1Hz
482 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
483 if (dTime < 1000000) { //1hz
484 return;
486 previousTimeUs = currentTimeUs;
488 // checks that we are getting closer to home.
489 // if the quad is stuck, or if GPS data packets stop, there will be no change in distance to home
490 // we can't use rescueState.sensor.currentVelocity because it will be held at the last good value if GPS data updates stop
491 if (rescueState.phase == RESCUE_FLY_HOME) {
492 const float velocityToHomeCmS = previousDistanceToHomeCm- rescueState.sensor.distanceToHomeCm; // cm/s
493 previousDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
494 rescueState.intent.secondsFailing += (velocityToHomeCmS < 0.5f * rescueState.intent.targetVelocityCmS) ? 1 : -1;
495 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 15);
496 if (rescueState.intent.secondsFailing == 15) {
497 #ifdef USE_MAG
498 //If there is a mag and has not been disabled, we have to assume is healthy and has been used in imu.c
499 if (sensors(SENSOR_MAG) && gpsRescueConfig()->useMag && !magForceDisable) {
500 //Try again with mag disabled
501 magForceDisable = true;
502 rescueState.intent.secondsFailing = 0;
503 } else
504 #endif
506 rescueState.failure = RESCUE_FLYAWAY;
511 secondsLowSats += (!STATE(GPS_FIX) || (gpsSol.numSat < GPS_MIN_SAT_COUNT)) ? 1 : -1;
512 secondsLowSats = constrain(secondsLowSats, 0, 10);
514 if (secondsLowSats == 10) {
515 rescueState.failure = RESCUE_LOWSATS;
519 // These conditions ignore sanity mode settings, and apply in all rescues, to handle getting stuck in a climb or descend
521 const float actualAltitudeChange = rescueState.sensor.currentAltitudeCm - prevAltitudeCm;
522 const float targetAltitudeChange = rescueState.intent.targetAltitudeCm - prevTargetAltitudeCm;
523 const float ratio = actualAltitudeChange / targetAltitudeChange;
524 prevAltitudeCm = rescueState.sensor.currentAltitudeCm;
525 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
527 if (rescueState.phase == RESCUE_LANDING) {
528 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
529 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
530 if (rescueState.intent.secondsFailing == 10) {
531 rescueState.phase = RESCUE_ABORT;
532 // Landing mode shouldn't take more than 10s
534 } else if (rescueState.phase == RESCUE_ATTAIN_ALT || rescueState.phase == RESCUE_DESCENT) {
535 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
536 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
537 if (rescueState.intent.secondsFailing == 10) {
538 rescueState.phase = RESCUE_LANDING;
539 rescueState.intent.secondsFailing = 0;
540 // if can't climb, or slow descending, enable impact detection and time out in 10s
542 } else if (rescueState.phase == RESCUE_DO_NOTHING) {
543 secondsDoingNothing = MIN(secondsDoingNothing + 1, 20);
544 if (secondsDoingNothing == 20) {
545 rescueState.phase = RESCUE_ABORT;
546 // time-limited semi-controlled fall with impact detection
550 DEBUG_SET(DEBUG_RTH, 2, (rescueState.failure * 10 + rescueState.phase));
551 DEBUG_SET(DEBUG_RTH, 3, (rescueState.intent.secondsFailing * 100 + secondsLowSats));
554 static void sensorUpdate(void)
556 static float prevDistanceToHomeCm = 0.0f;
557 const timeUs_t currentTimeUs = micros();
559 static timeUs_t previousAltitudeDataTimeUs = 0;
560 const timeDelta_t altitudeDataIntervalUs = cmpTimeUs(currentTimeUs, previousAltitudeDataTimeUs);
561 rescueState.sensor.altitudeDataIntervalSeconds = altitudeDataIntervalUs * 0.000001f;
562 previousAltitudeDataTimeUs = currentTimeUs;
564 rescueState.sensor.currentAltitudeCm = getAltitude();
566 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 2, lrintf(rescueState.sensor.currentAltitudeCm));
567 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 2, lrintf(rescueState.sensor.currentAltitudeCm));
568 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 0, rescueState.sensor.groundSpeedCmS); // groundspeed cm/s
569 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 1, gpsSol.groundCourse); // degrees * 10
570 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 2, attitude.values.yaw); // degrees * 10
571 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 3, rescueState.sensor.directionToHome); // degrees * 10
573 rescueState.sensor.healthy = gpsIsHealthy();
575 if (rescueState.phase == RESCUE_LANDING) {
576 // do this at sensor update rate, not the much slower GPS rate, for quick disarm
577 rescueState.sensor.accMagnitude = (float) sqrtf(sq(acc.accADC[Z]) + sq(acc.accADC[X]) + sq(acc.accADC[Y])) * acc.dev.acc_1G_rec;
580 rescueState.sensor.directionToHome = GPS_directionToHome;
581 rescueState.sensor.errorAngle = (attitude.values.yaw - rescueState.sensor.directionToHome) * 0.1f;
582 // both attitude and direction are in degrees * 10, errorAngle is degrees
583 if (rescueState.sensor.errorAngle <= -180) {
584 rescueState.sensor.errorAngle += 360;
585 } else if (rescueState.sensor.errorAngle > 180) {
586 rescueState.sensor.errorAngle -= 360;
588 rescueState.sensor.absErrorAngle = fabsf(rescueState.sensor.errorAngle);
590 if (!newGPSData) {
591 return;
592 // GPS ground speed, velocity and distance to home will be held at last good values if no new packets
595 rescueState.sensor.distanceToHomeCm = GPS_distanceToHomeCm;
596 rescueState.sensor.distanceToHomeM = rescueState.sensor.distanceToHomeCm / 100.0f;
597 rescueState.sensor.groundSpeedCmS = gpsSol.groundSpeed; // cm/s
599 static timeUs_t previousGPSDataTimeUs = 0;
600 const timeDelta_t gpsDataIntervalUs = cmpTimeUs(currentTimeUs, previousGPSDataTimeUs);
601 rescueState.sensor.gpsDataIntervalSeconds = constrainf(gpsDataIntervalUs * 0.000001f, 0.01f, 1.0f);
602 // Range from 10ms (100hz) to 1000ms (1Hz). Intended to cover common GPS data rates and exclude unusual values.
603 previousGPSDataTimeUs = currentTimeUs;
605 rescueState.sensor.filterK = pt1FilterGain(0.8, rescueState.sensor.gpsDataIntervalSeconds);
606 // 0.8341 for 1hz, 0.5013 for 5hz, 0.3345 for 10hz, 0.1674 for 25Hz, etc
608 rescueState.sensor.velocityToHomeCmS = (prevDistanceToHomeCm - rescueState.sensor.distanceToHomeCm) / rescueState.sensor.gpsDataIntervalSeconds;
609 // positive = towards home. First value is useless since prevDistanceToHomeCm was zero.
610 prevDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
612 rescueState.sensor.maxPitchStep = rescueState.sensor.gpsDataIntervalSeconds * GPS_RESCUE_MAX_PITCH_RATE;
614 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 2, lrintf(rescueState.sensor.velocityToHomeCmS));
615 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 0, lrintf(rescueState.sensor.velocityToHomeCmS));
619 // This function flashes "RESCUE N/A" in the OSD if:
620 // 1. sensor healthy - GPS data is being received.
621 // 2. GPS has a 3D fix.
622 // 3. GPS number of satellites is greater than or equal to the minimum configured satellite count.
623 // Note 1: cannot arm without the required number of sats
624 // hence this flashing indicates that after having enough sats, we now have below the minimum and the rescue likely would fail
625 // Note 2: this function does not take into account the distance from home
626 // The sanity checks are independent, this just provides the OSD warning
627 static bool checkGPSRescueIsAvailable(void)
629 static timeUs_t previousTimeUs = 0; // Last time LowSat was checked
630 const timeUs_t currentTimeUs = micros();
631 static int8_t secondsLowSats = 0; // Minimum sat detection
632 static bool lowsats = false;
633 static bool noGPSfix = false;
634 bool result = true;
636 if (!gpsIsHealthy() || !STATE(GPS_FIX_HOME)) {
637 return false;
640 // Things that should run at a low refresh rate >> ~1hz
641 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
642 if (dTime < 1000000) { //1hz
643 if (noGPSfix || lowsats) {
644 result = false;
646 return result;
649 previousTimeUs = currentTimeUs;
651 if (!STATE(GPS_FIX)) {
652 result = false;
653 noGPSfix = true;
654 } else {
655 noGPSfix = false;
658 secondsLowSats = constrain(secondsLowSats + ((gpsSol.numSat < GPS_MIN_SAT_COUNT) ? 1 : -1), 0, 2);
659 if (secondsLowSats == 2) {
660 lowsats = true;
661 result = false;
662 } else {
663 lowsats = false;
666 return result;
669 void disarmOnImpact(void)
671 if (rescueState.sensor.accMagnitude > rescueState.intent.disarmThreshold) {
672 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
673 disarm(DISARM_REASON_GPS_RESCUE);
674 rescueStop();
678 void descend(void)
680 if (newGPSData) {
681 const float distanceToLandingAreaM = rescueState.sensor.distanceToHomeM - (rescueState.intent.targetLandingAltitudeCm / 200.0f);
682 // considers home to be a circle half landing height around home to avoid overshooting home point
683 const float proximityToLandingArea = constrainf(distanceToLandingAreaM / rescueState.intent.descentDistanceM, 0.0f, 1.0f);
684 rescueState.intent.targetVelocityCmS = gpsRescueConfig()->rescueGroundspeed * proximityToLandingArea;
685 // reduce target velocity as we get closer to home. Zero within 2m of home, reducing risk of overshooting.
686 // if quad drifts further than 2m away from home, should by then have rotated towards home, so pitch is allowed
687 rescueState.intent.rollAngleLimitDeg = gpsRescueConfig()->angle * proximityToLandingArea;
688 // reduce roll capability when closer to home, none within final 2m
691 // adjust altitude step for interval between altitude readings
692 rescueState.intent.altitudeStep = -1.0f * rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
694 // descend more slowly if return altitude is less than 20m
695 const float descentAttenuator = rescueState.intent.returnAltitudeCm / 2000.0f;
696 if (descentAttenuator < 1.0f) {
697 rescueState.intent.altitudeStep *= descentAttenuator;
699 // descend more quickly from higher altitude
700 rescueState.intent.descentRateModifier = constrainf(rescueState.intent.targetAltitudeCm / 5000.0f, 0.0f, 1.0f);
701 rescueState.intent.targetAltitudeCm += rescueState.intent.altitudeStep * (1.0f + (2.0f * rescueState.intent.descentRateModifier));
702 // increase descent rate to max of 3x default above 50m, 2x above 25m, 1.2 at 5m, default by ground level.
705 void altitudeAchieved(void)
707 rescueState.intent.targetAltitudeCm = rescueState.intent.returnAltitudeCm;
708 rescueState.intent.altitudeStep = 0;
709 rescueState.phase = RESCUE_ROTATE;
712 void gpsRescueUpdate(void)
713 // this runs a lot faster than the GPS Data update rate, and runs whether or not rescue is active
715 if (!FLIGHT_MODE(GPS_RESCUE_MODE)) {
716 rescueStop(); // sets phase to RESCUE_IDLE; does nothing else. RESCUE_IDLE tasks still run.
717 } else if (FLIGHT_MODE(GPS_RESCUE_MODE) && rescueState.phase == RESCUE_IDLE) {
718 rescueStart(); // sets phase to rescue_initialise if we enter GPS Rescue mode while idle
719 rescueAttainPosition(); // Initialise basic parameters when a Rescue starts (can't initialise sensor data reliably)
720 performSanityChecks(); // Initialises sanity check values when a Rescue starts
723 // Will now be in RESCUE_INITIALIZE mode, if just entered Rescue while IDLE, otherwise stays IDLE
725 sensorUpdate(); // always get latest GPS and Altitude data, update ascend and descend rates
727 bool startedLow = true;
728 rescueState.isAvailable = checkGPSRescueIsAvailable();
730 switch (rescueState.phase) {
731 case RESCUE_IDLE:
732 // in Idle phase = NOT in GPS Rescue
733 // update the return altitude and descent distance values, to have valid settings immediately they are needed
734 setReturnAltitude();
735 break;
736 // sanity checks are bypassed in IDLE mode; instead, failure state is always initialised to HEALTHY
737 // target altitude is always set to current altitude.
739 case RESCUE_INITIALIZE:
740 // Things that should abort the start of a Rescue
741 if (!STATE(GPS_FIX_HOME)) {
742 // we didn't get a home point on arming
743 rescueState.failure = RESCUE_NO_HOME_POINT;
744 // will result in a disarm via the sanity check system, with delay if switch induced
745 // alternative is to prevent the rescue by returning to IDLE, but this could cause flyaways
746 } else if (rescueState.sensor.distanceToHomeM < gpsRescueConfig()->minRescueDth) {
747 // Attempt to initiate inside minimum activation distance -> landing mode
748 rescueState.intent.altitudeStep = -rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
749 rescueState.intent.targetVelocityCmS = 0; // zero forward velocity
750 rescueState.intent.pitchAngleLimitDeg = 0; // flat on pitch
751 rescueState.intent.rollAngleLimitDeg = 0.0f; // flat on roll also
752 rescueState.intent.targetAltitudeCm = rescueState.sensor.currentAltitudeCm + rescueState.intent.altitudeStep;
753 rescueState.phase = RESCUE_LANDING;
754 // start landing from current altitude
755 } else {
756 rescueState.phase = RESCUE_ATTAIN_ALT;
757 rescueState.intent.secondsFailing = 0; // reset the sanity check timer for the climb
758 rescueState.intent.targetLandingAltitudeCm = 100.0f * gpsRescueConfig()->targetLandingAltitudeM;
759 startedLow = (rescueState.sensor.currentAltitudeCm <= rescueState.intent.returnAltitudeCm);
760 rescueState.intent.yawAttenuator = 0.0f;
761 rescueState.intent.targetVelocityCmS = 0.0f; // zero forward velocity while climbing
762 rescueState.intent.pitchAngleLimitDeg = 0.0f; // no pitch
763 rescueState.intent.rollAngleLimitDeg = 0.0f; // no roll until flying home
764 rescueState.intent.altitudeStep = 0.0f;
765 rescueState.intent.descentRateModifier = 0.0f;
767 break;
769 case RESCUE_ATTAIN_ALT:
770 // gradually increment the target altitude until the craft reaches target altitude
771 // note that this can mean the target altitude may increase above returnAltitude if the craft lags target
772 // sanity check will abort if altitude gain is blocked for a cumulative period
773 if (startedLow) {
774 if (rescueState.intent.targetAltitudeCm < rescueState.intent.returnAltitudeCm) {
775 rescueState.intent.altitudeStep = rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->ascendRate;
776 } else if (rescueState.sensor.currentAltitudeCm > rescueState.intent.returnAltitudeCm) {
777 altitudeAchieved();
779 } else {
780 if (rescueState.intent.targetAltitudeCm > rescueState.intent.returnAltitudeCm) {
781 rescueState.intent.altitudeStep = -rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
782 } else if (rescueState.sensor.currentAltitudeCm < rescueState.intent.returnAltitudeCm) {
783 altitudeAchieved();
786 rescueState.intent.targetAltitudeCm += rescueState.intent.altitudeStep;
787 break;
789 case RESCUE_ROTATE:
790 if (rescueState.intent.yawAttenuator < 1.0f) { // gradually acquire yaw authority
791 rescueState.intent.yawAttenuator += 0.01f;
793 if (rescueState.sensor.absErrorAngle < 30.0f) {
794 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->angle; // allow pitch
795 rescueState.phase = RESCUE_FLY_HOME; // enter fly home phase
796 rescueState.intent.secondsFailing = 0; // reset sanity timer for flight home
798 break;
800 case RESCUE_FLY_HOME:
801 if (rescueState.intent.yawAttenuator < 1.0f) { // be sure to accumulate full yaw authority
802 rescueState.intent.yawAttenuator += 0.01f;
804 // steadily increase target velocity target until full return velocity is acquired
805 if (rescueState.intent.targetVelocityCmS < gpsRescueConfig()->rescueGroundspeed) {
806 rescueState.intent.targetVelocityCmS += 0.01f * gpsRescueConfig()->rescueGroundspeed;
808 // acquire full roll authority slowly when pointing to home
809 if (rescueState.sensor.absErrorAngle < 10.0f && rescueState.intent.rollAngleLimitDeg < gpsRescueConfig()->angle) {
810 // roll is primarily intended to deal with wind drift causing small yaw errors during return
811 rescueState.intent.rollAngleLimitDeg += 0.1f;
814 if (newGPSData) {
815 if (rescueState.sensor.distanceToHomeM <= rescueState.intent.descentDistanceM) {
816 rescueState.phase = RESCUE_DESCENT;
817 rescueState.intent.secondsFailing = 0; // reset sanity timer for descent
820 break;
822 case RESCUE_DESCENT:
823 // attenuate velocity and altitude targets while updating the heading to home
824 if (rescueState.sensor.currentAltitudeCm < rescueState.intent.targetLandingAltitudeCm) {
825 // enter landing mode once below landing altitude
826 rescueState.phase = RESCUE_LANDING;
827 rescueState.intent.secondsFailing = 0; // reset sanity timer for landing
829 descend();
830 break;
832 case RESCUE_LANDING:
833 // Reduce altitude target steadily until impact, then disarm.
834 // control yaw angle and throttle and pitch, attenuate velocity, roll and pitch iTerm
835 descend();
836 disarmOnImpact();
837 break;
839 case RESCUE_COMPLETE:
840 rescueStop();
841 break;
843 case RESCUE_ABORT:
844 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
845 disarm(DISARM_REASON_FAILSAFE);
846 rescueStop();
847 break;
849 case RESCUE_DO_NOTHING:
850 disarmOnImpact();
851 break;
853 default:
854 break;
857 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 3, lrintf(rescueState.intent.targetAltitudeCm));
858 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 3, lrintf(rescueState.intent.targetAltitudeCm));
859 DEBUG_SET(DEBUG_RTH, 0, lrintf(rescueState.intent.maxAltitudeCm));
861 performSanityChecks();
862 rescueAttainPosition();
864 newGPSData = false;
867 float gpsRescueGetYawRate(void)
869 return rescueYaw;
872 float gpsRescueGetThrottle(void)
874 // Calculated a desired commanded throttle scaled from 0.0 to 1.0 for use in the mixer.
875 // We need to compensate for min_check since the throttle value set by gps rescue
876 // is based on the raw rcCommand value commanded by the pilot.
877 float commandedThrottle = scaleRangef(rescueThrottle, MAX(rxConfig()->mincheck, PWM_RANGE_MIN), PWM_RANGE_MAX, 0.0f, 1.0f);
878 commandedThrottle = constrainf(commandedThrottle, 0.0f, 1.0f);
880 return commandedThrottle;
883 bool gpsRescueIsConfigured(void)
885 return failsafeConfig()->failsafe_procedure == FAILSAFE_PROCEDURE_GPS_RESCUE || isModeActivationConditionPresent(BOXGPSRESCUE);
888 bool gpsRescueIsAvailable(void)
890 return rescueState.isAvailable;
893 bool gpsRescueIsDisabled(void)
894 // used for OSD warning
896 return (!STATE(GPS_FIX_HOME));
899 #ifdef USE_MAG
900 bool gpsRescueDisableMag(void)
902 return ((!gpsRescueConfig()->useMag || magForceDisable) && (rescueState.phase >= RESCUE_INITIALIZE) && (rescueState.phase <= RESCUE_LANDING));
904 #endif
905 #endif