Implement Stopwatch (#12623)
[betaflight.git] / src / main / flight / gps_rescue.c
blob14a18932b7e0819fa5299aecaee3dc985b75536c
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 "drivers/time.h"
35 #include "io/gps.h"
37 #include "config/config.h"
38 #include "fc/core.h"
39 #include "fc/rc_controls.h"
40 #include "fc/rc_modes.h"
41 #include "fc/runtime_config.h"
43 #include "flight/failsafe.h"
44 #include "flight/imu.h"
45 #include "flight/pid.h"
46 #include "flight/position.h"
48 #include "rx/rx.h"
50 #include "sensors/acceleration.h"
52 #include "gps_rescue.h"
54 typedef enum {
55 RESCUE_IDLE,
56 RESCUE_INITIALIZE,
57 RESCUE_ATTAIN_ALT,
58 RESCUE_ROTATE,
59 RESCUE_FLY_HOME,
60 RESCUE_DESCENT,
61 RESCUE_LANDING,
62 RESCUE_ABORT,
63 RESCUE_COMPLETE,
64 RESCUE_DO_NOTHING
65 } rescuePhase_e;
67 typedef enum {
68 RESCUE_HEALTHY,
69 RESCUE_FLYAWAY,
70 RESCUE_GPSLOST,
71 RESCUE_LOWSATS,
72 RESCUE_CRASH_FLIP_DETECTED,
73 RESCUE_STALLED,
74 RESCUE_TOO_CLOSE,
75 RESCUE_NO_HOME_POINT
76 } rescueFailureState_e;
78 typedef struct {
79 float maxAltitudeCm;
80 float returnAltitudeCm;
81 float targetAltitudeCm;
82 float targetLandingAltitudeCm;
83 float targetVelocityCmS;
84 float pitchAngleLimitDeg;
85 float rollAngleLimitDeg;
86 float descentDistanceM;
87 int8_t secondsFailing;
88 float altitudeStep;
89 float descentRateModifier;
90 float yawAttenuator;
91 float disarmThreshold;
92 float velocityITermAccumulator;
93 float velocityPidCutoff;
94 float velocityPidCutoffModifier;
95 float proximityToLandingArea;
96 float velocityItermRelax;
97 } rescueIntent_s;
99 typedef struct {
100 float currentAltitudeCm;
101 float distanceToHomeCm;
102 float distanceToHomeM;
103 uint16_t groundSpeedCmS;
104 int16_t directionToHome;
105 float accMagnitude;
106 bool healthy;
107 float errorAngle;
108 float gpsDataIntervalSeconds;
109 float altitudeDataIntervalSeconds;
110 float gpsRescueTaskIntervalSeconds;
111 float velocityToHomeCmS;
112 float alitutudeStepCm;
113 float maxPitchStep;
114 float absErrorAngle;
115 } rescueSensorData_s;
117 typedef struct {
118 rescuePhase_e phase;
119 rescueFailureState_e failure;
120 rescueSensorData_s sensor;
121 rescueIntent_s intent;
122 bool isAvailable;
123 } rescueState_s;
125 #define GPS_RESCUE_MAX_YAW_RATE 180 // deg/sec max yaw rate
126 #define GPS_RESCUE_MIN_DESCENT_DIST_M 5 // minimum descent distance
127 #define GPS_RESCUE_MAX_THROTTLE_ITERM 200 // max iterm value for throttle in degrees * 100
129 static float rescueThrottle;
130 static float rescueYaw;
131 float gpsRescueAngle[ANGLE_INDEX_COUNT] = { 0, 0 };
132 bool magForceDisable = false;
133 static bool newGPSData = false;
134 static pt2Filter_t throttleDLpf;
135 static pt1Filter_t velocityDLpf;
136 static pt3Filter_t velocityUpsampleLpf;
138 rescueState_s rescueState;
140 void gpsRescueInit(void)
142 rescueState.sensor.gpsRescueTaskIntervalSeconds = HZ_TO_INTERVAL(TASK_GPS_RESCUE_RATE_HZ);
144 float cutoffHz, gain;
145 cutoffHz = positionConfig()->altitude_d_lpf / 100.0f;
146 gain = pt2FilterGain(cutoffHz, rescueState.sensor.gpsRescueTaskIntervalSeconds);
147 pt2FilterInit(&throttleDLpf, gain);
149 cutoffHz = gpsRescueConfig()->pitchCutoffHz / 100.0f;
150 rescueState.intent.velocityPidCutoff = cutoffHz;
151 rescueState.intent.velocityPidCutoffModifier = 1.0f;
152 gain = pt1FilterGain(cutoffHz, 1.0f);
153 pt1FilterInit(&velocityDLpf, gain);
155 cutoffHz *= 4.0f;
156 gain = pt3FilterGain(cutoffHz, rescueState.sensor.gpsRescueTaskIntervalSeconds);
157 pt3FilterInit(&velocityUpsampleLpf, gain);
161 If we have new GPS data, update home heading if possible and applicable.
163 void gpsRescueNewGpsData(void)
165 newGPSData = true;
168 static void rescueStart(void)
170 rescueState.phase = RESCUE_INITIALIZE;
173 static void rescueStop(void)
175 rescueState.phase = RESCUE_IDLE;
178 // Things that need to run when GPS Rescue is enabled, and while armed, but while there is no Rescue in place
179 static void setReturnAltitude(void)
181 // Hold maxAltitude at zero while disarmed, but if set_home_point_once is true, hold maxAlt until power cycled
182 if (!ARMING_FLAG(ARMED) && !gpsConfig()->gps_set_home_point_once) {
183 rescueState.intent.maxAltitudeCm = 0.0f;
184 return;
187 // While armed, but not during the rescue, update the max altitude value
188 rescueState.intent.maxAltitudeCm = fmaxf(rescueState.sensor.currentAltitudeCm, rescueState.intent.maxAltitudeCm);
190 if (newGPSData) {
191 // set the target altitude to current values, so there will be no D kick on first run
192 rescueState.intent.targetAltitudeCm = rescueState.sensor.currentAltitudeCm;
194 // Keep the descent distance and intended altitude up to date with latest GPS values
195 rescueState.intent.descentDistanceM = constrainf(rescueState.sensor.distanceToHomeM, GPS_RESCUE_MIN_DESCENT_DIST_M, gpsRescueConfig()->descentDistanceM);
196 const float initialAltitudeCm = gpsRescueConfig()->initialAltitudeM * 100.0f;
197 const float rescueAltitudeBufferCm = gpsRescueConfig()->rescueAltitudeBufferM * 100.0f;
198 switch (gpsRescueConfig()->altitudeMode) {
199 case GPS_RESCUE_ALT_MODE_FIXED:
200 rescueState.intent.returnAltitudeCm = initialAltitudeCm;
201 break;
202 case GPS_RESCUE_ALT_MODE_CURRENT:
203 rescueState.intent.returnAltitudeCm = rescueState.sensor.currentAltitudeCm + rescueAltitudeBufferCm;
204 break;
205 case GPS_RESCUE_ALT_MODE_MAX:
206 default:
207 rescueState.intent.returnAltitudeCm = rescueState.intent.maxAltitudeCm + rescueAltitudeBufferCm;
208 break;
213 static void rescueAttainPosition(void)
215 // runs at 100hz, but only updates RPYT settings when new GPS Data arrives and when not in idle phase.
216 static float previousVelocityError = 0.0f;
217 static float velocityI = 0.0f;
218 static float throttleI = 0.0f;
219 static float previousAltitudeError = 0.0f;
220 static int16_t throttleAdjustment = 0;
222 switch (rescueState.phase) {
223 case RESCUE_IDLE:
224 // values to be returned when no rescue is active
225 gpsRescueAngle[AI_PITCH] = 0.0f;
226 gpsRescueAngle[AI_ROLL] = 0.0f;
227 rescueThrottle = rcCommand[THROTTLE];
228 return;
229 case RESCUE_INITIALIZE:
230 // Initialize internal variables each time GPS Rescue is started
231 previousVelocityError = 0.0f;
232 velocityI = 0.0f;
233 throttleI = 0.0f;
234 previousAltitudeError = 0.0f;
235 throttleAdjustment = 0;
236 rescueState.intent.disarmThreshold = gpsRescueConfig()->disarmThreshold / 10.0f;
237 return;
238 case RESCUE_DO_NOTHING:
239 // 20s of slow descent for switch induced sanity failures to allow time to recover
240 gpsRescueAngle[AI_PITCH] = 0.0f;
241 gpsRescueAngle[AI_ROLL] = 0.0f;
242 rescueThrottle = gpsRescueConfig()->throttleHover - 100;
243 return;
244 default:
245 break;
249 Altitude (throttle) controller
251 // currentAltitudeCm is updated at TASK_GPS_RESCUE_RATE_HZ
252 const float altitudeError = (rescueState.intent.targetAltitudeCm - rescueState.sensor.currentAltitudeCm) * 0.01f;
253 // height above target in metres (negative means too low)
254 // at the start, the target starts at current altitude plus one step. Increases stepwise to intended value.
256 // P component
257 const float throttleP = gpsRescueConfig()->throttleP * altitudeError;
259 // I component
260 throttleI += 0.1f * gpsRescueConfig()->throttleI * altitudeError * rescueState.sensor.altitudeDataIntervalSeconds;
261 throttleI = constrainf(throttleI, -1.0f * GPS_RESCUE_MAX_THROTTLE_ITERM, 1.0f * GPS_RESCUE_MAX_THROTTLE_ITERM);
262 // up to 20% increase in throttle from I alone
264 // D component is error based, so includes positive boost when climbing and negative boost on descent
265 float verticalSpeed = ((altitudeError - previousAltitudeError) / rescueState.sensor.altitudeDataIntervalSeconds);
266 previousAltitudeError = altitudeError;
267 verticalSpeed += rescueState.intent.descentRateModifier * verticalSpeed;
268 // add up to 2x D when descent rate is faster
270 float throttleD = pt2FilterApply(&throttleDLpf, verticalSpeed);
272 throttleD = gpsRescueConfig()->throttleD * throttleD;
274 // acceleration component not currently implemented - was needed previously due to GPS lag, maybe not needed now.
276 float tiltAdjustment = 1.0f - getCosTiltAngle(); // 0 = flat, gets to 0.2 correcting on a windy day
277 tiltAdjustment *= (gpsRescueConfig()->throttleHover - 1000);
278 // if hover is 1300, and adjustment .2, this gives us 0.2*300 or 60 of extra throttle, not much, but useful
279 // too much and landings with lots of pitch adjustment, eg windy days, can be a problem
281 throttleAdjustment = throttleP + throttleI + throttleD + tiltAdjustment;
283 rescueThrottle = gpsRescueConfig()->throttleHover + throttleAdjustment;
284 rescueThrottle = constrainf(rescueThrottle, gpsRescueConfig()->throttleMin, gpsRescueConfig()->throttleMax);
285 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 0, lrintf(throttleP));
286 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 1, lrintf(throttleD));
289 Heading / yaw controller
291 // simple yaw P controller with roll mixed in.
292 // attitude.values.yaw is set by imuCalculateEstimatedAttitude() and is updated from GPS while groundspeed exceeds 2 m/s
293 // below 2m/s groundspeed, the IMU uses gyro to estimate yaw attitude change from previous values
294 // above 2m/s, GPS course over ground us ysed to 'correct' the IMU heading
295 // 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
296 // the craft should not return much less than 5m/s during the rescue or the GPS corrections may be inaccurate.
297 // the faster the return speed, the more accurate the IMU will be, but the consequences of IMU error at the start are greater
298 // A compass (magnetometer) is vital for accurate GPS rescue at slow speeds, but must be calibrated and validated
299 // WARNING: Some GPS units give false Home values! Always check the arrow points to home on leaving home.
300 rescueYaw = rescueState.sensor.errorAngle * gpsRescueConfig()->yawP * rescueState.intent.yawAttenuator * 0.1f;
301 rescueYaw = constrainf(rescueYaw, -GPS_RESCUE_MAX_YAW_RATE, GPS_RESCUE_MAX_YAW_RATE);
302 // rescueYaw is the yaw rate in deg/s to correct the heading error
304 // *** mix in some roll. very important for heading tracking, since a yaw rate means the quad has drifted sideways
305 const float rollMixAttenuator = constrainf(1.0f - fabsf(rescueYaw) * 0.01f, 0.0f, 1.0f);
306 // less roll at higher yaw rates, no roll at 100 deg/s of yaw
307 const float rollAdjustment = -rescueYaw * gpsRescueConfig()->rollMix * rollMixAttenuator;
308 // 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
309 // when gpsRescueConfig()->rollMix is zero, there is no roll adjustment
310 // rollAdjustment is degrees * 100
311 // note that the roll element has the opposite sign to the yaw element *before* GET_DIRECTION
312 const float rollLimit = 100.0f * rescueState.intent.rollAngleLimitDeg;
313 gpsRescueAngle[AI_ROLL] = constrainf(rollAdjustment, -rollLimit, rollLimit);
314 // gpsRescueAngle is added to the normal roll Angle Mode corrections in pid.c
316 rescueYaw *= GET_DIRECTION(rcControlsConfig()->yaw_control_reversed);
317 // rescueYaw is the yaw rate in deg/s to correct the heading error
320 Pitch / velocity controller
322 static float pitchAdjustment = 0.0f;
323 if (newGPSData) {
325 const float sampleIntervalNormaliseFactor = rescueState.sensor.gpsDataIntervalSeconds * 10.0f;
327 const float velocityError = rescueState.intent.targetVelocityCmS - rescueState.sensor.velocityToHomeCmS;
328 // velocityError is in cm per second, positive means too slow.
329 // NB positive pitch setpoint means nose down.
330 // target velocity can be very negative leading to large error before the start, with overshoot
332 // P component
333 const float velocityP = velocityError * gpsRescueConfig()->velP;
335 // I component
336 velocityI += 0.01f * gpsRescueConfig()->velI * velocityError * sampleIntervalNormaliseFactor * rescueState.intent.velocityItermRelax;
337 // velocityItermRelax is a time-based factor, 0->1 with time constant of 1s from when we start to fly home
338 // avoids excess iTerm during the initial acceleration phase.
339 velocityI *= rescueState.intent.proximityToLandingArea;
340 // reduce iTerm sharply when velocity decreases in landing phase, to minimise overshoot during deceleration
342 const float pitchAngleLimit = rescueState.intent.pitchAngleLimitDeg * 100.0f;
343 const float velocityPILimit = 0.5f * pitchAngleLimit;
344 velocityI = constrainf(velocityI, -velocityPILimit, velocityPILimit);
345 // I component alone cannot exceed half the max pitch angle
347 // D component
348 float velocityD = ((velocityError - previousVelocityError) / sampleIntervalNormaliseFactor);
349 previousVelocityError = velocityError;
350 velocityD *= gpsRescueConfig()->velD;
352 // smooth the D steps
353 const float cutoffHz = rescueState.intent.velocityPidCutoff * rescueState.intent.velocityPidCutoffModifier;
354 // note that this cutoff is increased up to 2x as we get closer to landing point in descend()
355 const float gain = pt1FilterGain(cutoffHz, rescueState.sensor.gpsDataIntervalSeconds);
356 pt1FilterUpdateCutoff(&velocityDLpf, gain);
357 velocityD = pt1FilterApply(&velocityDLpf, velocityD);
359 pitchAdjustment = velocityP + velocityI + velocityD;
360 pitchAdjustment = constrainf(pitchAdjustment, -pitchAngleLimit, pitchAngleLimit);
361 // limit to maximum allowed angle
363 // pitchAdjustment is the absolute Pitch angle adjustment value in degrees * 100
364 // it gets added to the normal level mode Pitch adjustments in pid.c
365 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 0, lrintf(velocityP));
366 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 1, lrintf(velocityD));
369 // upsampling and smoothing of pitch angle steps
370 float pitchAdjustmentFiltered = pt3FilterApply(&velocityUpsampleLpf, pitchAdjustment);
373 gpsRescueAngle[AI_PITCH] = pitchAdjustmentFiltered;
374 // this angle gets added to the normal pitch Angle Mode control values in pid.c - will be seen in pitch setpoint
376 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 3, lrintf(rescueState.intent.targetVelocityCmS));
377 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 1, lrintf(rescueState.intent.targetVelocityCmS));
380 static void performSanityChecks(void)
382 static timeUs_t previousTimeUs = 0; // Last time Stalled/LowSat was checked
383 static float prevAltitudeCm = 0.0f; // to calculate ascent or descent change
384 static float prevTargetAltitudeCm = 0.0f; // to calculate ascent or descent target change
385 static float previousDistanceToHomeCm = 0.0f; // to check that we are returning
386 static int8_t secondsLowSats = 0; // Minimum sat detection
387 static int8_t secondsDoingNothing; // Limit on doing nothing
388 const timeUs_t currentTimeUs = micros();
390 if (rescueState.phase == RESCUE_IDLE) {
391 rescueState.failure = RESCUE_HEALTHY;
392 return;
393 } else if (rescueState.phase == RESCUE_INITIALIZE) {
394 // Initialize these variables each time a GPS Rescue is started
395 previousTimeUs = currentTimeUs;
396 prevAltitudeCm = rescueState.sensor.currentAltitudeCm;
397 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
398 previousDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
399 secondsLowSats = 0;
400 secondsDoingNothing = 0;
403 // Handle events that set a failure mode to other than healthy.
404 // Disarm via Abort when sanity on, or for hard Rx loss in FS_ONLY mode
405 // Otherwise allow 20s of semi-controlled descent with impact disarm detection
406 const bool hardFailsafe = !rxIsReceivingSignal();
408 if (rescueState.failure != RESCUE_HEALTHY) {
409 // Default to 20s semi-controlled descent with impact detection, then abort
410 rescueState.phase = RESCUE_DO_NOTHING;
412 switch(gpsRescueConfig()->sanityChecks) {
413 case RESCUE_SANITY_ON:
414 rescueState.phase = RESCUE_ABORT;
415 break;
416 case RESCUE_SANITY_FS_ONLY:
417 if (hardFailsafe) {
418 rescueState.phase = RESCUE_ABORT;
420 break;
421 default:
422 // even with sanity checks off,
423 // override when Allow Arming without Fix is enabled without GPS_FIX_HOME and no Control link available.
424 if (gpsRescueConfig()->allowArmingWithoutFix && !STATE(GPS_FIX_HOME) && hardFailsafe) {
425 rescueState.phase = RESCUE_ABORT;
430 // Crash detection is enabled in all rescues. If triggered, immediately disarm.
431 if (crashRecoveryModeActive()) {
432 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
433 disarm(DISARM_REASON_CRASH_PROTECTION);
434 rescueStop();
437 // Check if GPS comms are healthy
438 // ToDo - check if we have an altitude reading; if we have Baro, we can use Landing mode for controlled descent without GPS
439 if (!rescueState.sensor.healthy) {
440 rescueState.failure = RESCUE_GPSLOST;
443 // Things that should run at a low refresh rate (such as flyaway detection, etc) will be checked at 1Hz
444 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
445 if (dTime < 1000000) { //1hz
446 return;
448 previousTimeUs = currentTimeUs;
450 // checks that we are getting closer to home.
451 // if the quad is stuck, or if GPS data packets stop, there will be no change in distance to home
452 // we can't use rescueState.sensor.currentVelocity because it will be held at the last good value if GPS data updates stop
453 if (rescueState.phase == RESCUE_FLY_HOME) {
454 const float velocityToHomeCmS = previousDistanceToHomeCm - rescueState.sensor.distanceToHomeCm; // cm/s
455 previousDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
456 rescueState.intent.secondsFailing += (velocityToHomeCmS < 0.5f * rescueState.intent.targetVelocityCmS) ? 1 : -1;
457 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 15);
458 if (rescueState.intent.secondsFailing == 15) {
459 #ifdef USE_MAG
460 //If there is a mag and has not been disabled, we have to assume is healthy and has been used in imu.c
461 if (sensors(SENSOR_MAG) && gpsRescueConfig()->useMag && !magForceDisable) {
462 //Try again with mag disabled
463 magForceDisable = true;
464 rescueState.intent.secondsFailing = 0;
465 } else
466 #endif
468 rescueState.failure = RESCUE_FLYAWAY;
473 secondsLowSats += (!STATE(GPS_FIX) || (gpsSol.numSat < GPS_MIN_SAT_COUNT)) ? 1 : -1;
474 secondsLowSats = constrain(secondsLowSats, 0, 10);
476 if (secondsLowSats == 10) {
477 rescueState.failure = RESCUE_LOWSATS;
481 // These conditions ignore sanity mode settings, and apply in all rescues, to handle getting stuck in a climb or descend
483 const float actualAltitudeChange = rescueState.sensor.currentAltitudeCm - prevAltitudeCm;
484 const float targetAltitudeChange = rescueState.intent.targetAltitudeCm - prevTargetAltitudeCm;
485 const float ratio = actualAltitudeChange / targetAltitudeChange;
486 prevAltitudeCm = rescueState.sensor.currentAltitudeCm;
487 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
489 switch (rescueState.phase) {
490 case RESCUE_LANDING:
491 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
492 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
493 if (rescueState.intent.secondsFailing == 10) {
494 rescueState.phase = RESCUE_ABORT;
495 // Landing mode shouldn't take more than 10s
497 break;
498 case RESCUE_ATTAIN_ALT:
499 case RESCUE_DESCENT:
500 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
501 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
502 if (rescueState.intent.secondsFailing == 10) {
503 rescueState.phase = RESCUE_LANDING;
504 rescueState.intent.secondsFailing = 0;
505 // if can't climb, or slow descending, enable impact detection and time out in 10s
507 break;
508 case RESCUE_DO_NOTHING:
509 secondsDoingNothing = MIN(secondsDoingNothing + 1, 20);
510 if (secondsDoingNothing == 20) {
511 rescueState.phase = RESCUE_ABORT;
512 // time-limited semi-controlled fall with impact detection
514 break;
515 default:
516 // do nothing
517 break;
520 DEBUG_SET(DEBUG_RTH, 2, (rescueState.failure * 10 + rescueState.phase));
521 DEBUG_SET(DEBUG_RTH, 3, (rescueState.intent.secondsFailing * 100 + secondsLowSats));
524 static void sensorUpdate(void)
526 static float prevDistanceToHomeCm = 0.0f;
527 const timeUs_t currentTimeUs = micros();
529 static timeUs_t previousAltitudeDataTimeUs = 0;
530 const timeDelta_t altitudeDataIntervalUs = cmpTimeUs(currentTimeUs, previousAltitudeDataTimeUs);
531 rescueState.sensor.altitudeDataIntervalSeconds = altitudeDataIntervalUs * 0.000001f;
532 previousAltitudeDataTimeUs = currentTimeUs;
534 rescueState.sensor.currentAltitudeCm = getAltitude();
536 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 2, lrintf(rescueState.sensor.currentAltitudeCm));
537 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 2, lrintf(rescueState.sensor.currentAltitudeCm));
538 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 0, rescueState.sensor.groundSpeedCmS); // groundspeed cm/s
539 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 1, gpsSol.groundCourse); // degrees * 10
540 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 2, attitude.values.yaw); // degrees * 10
541 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 3, rescueState.sensor.directionToHome); // degrees * 10
543 rescueState.sensor.healthy = gpsIsHealthy();
545 if (rescueState.phase == RESCUE_LANDING) {
546 // do this at sensor update rate, not the much slower GPS rate, for quick disarm
547 rescueState.sensor.accMagnitude = (float) sqrtf(sq(acc.accADC[Z] - acc.dev.acc_1G) + sq(acc.accADC[X]) + sq(acc.accADC[Y])) * acc.dev.acc_1G_rec;
548 // Note: subtracting 1G from Z assumes the quad is 'flat' with respect to the horizon. A true non-gravity acceleration value, regardless of attitude, may be better.
551 rescueState.sensor.directionToHome = GPS_directionToHome;
552 rescueState.sensor.errorAngle = (attitude.values.yaw - rescueState.sensor.directionToHome) * 0.1f;
553 // both attitude and direction are in degrees * 10, errorAngle is degrees
554 if (rescueState.sensor.errorAngle <= -180) {
555 rescueState.sensor.errorAngle += 360;
556 } else if (rescueState.sensor.errorAngle > 180) {
557 rescueState.sensor.errorAngle -= 360;
559 rescueState.sensor.absErrorAngle = fabsf(rescueState.sensor.errorAngle);
561 if (!newGPSData) {
562 return;
563 // GPS ground speed, velocity and distance to home will be held at last good values if no new packets
566 rescueState.sensor.distanceToHomeCm = GPS_distanceToHomeCm;
567 rescueState.sensor.distanceToHomeM = rescueState.sensor.distanceToHomeCm / 100.0f;
568 rescueState.sensor.groundSpeedCmS = gpsSol.groundSpeed; // cm/s
570 rescueState.sensor.gpsDataIntervalSeconds = getGpsDataIntervalSeconds();
571 // Range from 10ms (100hz) to 1000ms (1Hz). Intended to cover common GPS data rates and exclude unusual values.
573 rescueState.sensor.velocityToHomeCmS = ((prevDistanceToHomeCm - rescueState.sensor.distanceToHomeCm) / rescueState.sensor.gpsDataIntervalSeconds);
574 // positive = towards home. First value is useless since prevDistanceToHomeCm was zero.
575 prevDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
577 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 2, lrintf(rescueState.sensor.velocityToHomeCmS));
578 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 0, lrintf(rescueState.sensor.velocityToHomeCmS));
581 // This function flashes "RESCUE N/A" in the OSD if:
582 // 1. sensor healthy - GPS data is being received.
583 // 2. GPS has a 3D fix.
584 // 3. GPS number of satellites is greater than or equal to the minimum configured satellite count.
585 // Note 1: cannot arm without the required number of sats
586 // hence this flashing indicates that after having enough sats, we now have below the minimum and the rescue likely would fail
587 // Note 2: this function does not take into account the distance from home
588 // The sanity checks are independent, this just provides the OSD warning
589 static bool checkGPSRescueIsAvailable(void)
591 static timeUs_t previousTimeUs = 0; // Last time LowSat was checked
592 const timeUs_t currentTimeUs = micros();
593 static int8_t secondsLowSats = 0; // Minimum sat detection
594 static bool lowsats = false;
595 static bool noGPSfix = false;
596 bool result = true;
598 if (!gpsIsHealthy() || !STATE(GPS_FIX_HOME)) {
599 return false;
602 // Things that should run at a low refresh rate >> ~1hz
603 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
604 if (dTime < 1000000) { //1hz
605 if (noGPSfix || lowsats) {
606 result = false;
608 return result;
611 previousTimeUs = currentTimeUs;
613 if (!STATE(GPS_FIX)) {
614 result = false;
615 noGPSfix = true;
616 } else {
617 noGPSfix = false;
620 secondsLowSats = constrain(secondsLowSats + ((gpsSol.numSat < GPS_MIN_SAT_COUNT) ? 1 : -1), 0, 2);
621 if (secondsLowSats == 2) {
622 lowsats = true;
623 result = false;
624 } else {
625 lowsats = false;
628 return result;
631 void disarmOnImpact(void)
633 if (rescueState.sensor.accMagnitude > rescueState.intent.disarmThreshold) {
634 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
635 disarm(DISARM_REASON_GPS_RESCUE);
636 rescueStop();
640 void descend(void)
642 if (newGPSData) {
643 const float distanceToLandingAreaM = rescueState.sensor.distanceToHomeM - (rescueState.intent.targetLandingAltitudeCm / 200.0f);
644 // considers home to be a circle half landing height around home to avoid overshooting home point
645 rescueState.intent.proximityToLandingArea = constrainf(distanceToLandingAreaM / rescueState.intent.descentDistanceM, 0.0f, 1.0f);
646 rescueState.intent.velocityPidCutoffModifier = 2.5f - rescueState.intent.proximityToLandingArea;
647 // 1.5 when starting descent, 2.5 when almost landed; multiplier for velocity step cutoff filter
648 rescueState.intent.targetVelocityCmS = gpsRescueConfig()->rescueGroundspeed * rescueState.intent.proximityToLandingArea;
649 // reduce target velocity as we get closer to home. Zero within 2m of home, reducing risk of overshooting.
650 // if quad drifts further than 2m away from home, should by then have rotated towards home, so pitch is allowed
651 rescueState.intent.rollAngleLimitDeg = gpsRescueConfig()->maxRescueAngle * rescueState.intent.proximityToLandingArea;
652 // reduce roll capability when closer to home, none within final 2m
655 // configure altitude step for descent, considering interval between altitude readings
656 rescueState.intent.altitudeStep = -1.0f * rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
658 // descend more slowly if return altitude is less than 20m
659 const float descentAttenuator = rescueState.intent.returnAltitudeCm / 2000.0f;
660 if (descentAttenuator < 1.0f) {
661 rescueState.intent.altitudeStep *= descentAttenuator;
663 // descend more quickly from higher altitude
664 rescueState.intent.descentRateModifier = constrainf(rescueState.intent.targetAltitudeCm / 5000.0f, 0.0f, 1.0f);
665 rescueState.intent.targetAltitudeCm += rescueState.intent.altitudeStep * (1.0f + (2.0f * rescueState.intent.descentRateModifier));
666 // increase descent rate to max of 3x default above 50m, 2x above 25m, 1.2 at 5m, default by ground level.
669 void gpsRescueUpdate(void)
670 // runs at gpsRescueTaskIntervalSeconds, and runs whether or not rescue is active
672 if (!FLIGHT_MODE(GPS_RESCUE_MODE)) {
673 rescueStop(); // sets phase to RESCUE_IDLE; does nothing else. RESCUE_IDLE tasks still run.
674 } else if (FLIGHT_MODE(GPS_RESCUE_MODE) && rescueState.phase == RESCUE_IDLE) {
675 rescueStart(); // sets phase to rescue_initialise if we enter GPS Rescue mode while idle
676 rescueAttainPosition(); // Initialise basic parameters when a Rescue starts (can't initialise sensor data reliably)
677 performSanityChecks(); // Initialises sanity check values when a Rescue starts
680 // Will now be in RESCUE_INITIALIZE mode, if just entered Rescue while IDLE, otherwise stays IDLE
682 sensorUpdate(); // always get latest GPS and Altitude data, update ascend and descend rates
684 static bool initialAltitudeLow = true;
685 static bool initialVelocityLow = true;
686 rescueState.isAvailable = checkGPSRescueIsAvailable();
688 switch (rescueState.phase) {
689 case RESCUE_IDLE:
690 // in Idle phase = NOT in GPS Rescue
691 // update the return altitude and descent distance values, to have valid settings immediately they are needed
692 setReturnAltitude();
693 break;
694 // sanity checks are bypassed in IDLE mode; instead, failure state is always initialised to HEALTHY
695 // target altitude is always set to current altitude.
697 case RESCUE_INITIALIZE:
698 // Things that should be done at the start of a Rescue
699 rescueState.intent.targetLandingAltitudeCm = 100.0f * gpsRescueConfig()->targetLandingAltitudeM;
700 if (!STATE(GPS_FIX_HOME)) {
701 // we didn't get a home point on arming
702 rescueState.failure = RESCUE_NO_HOME_POINT;
703 // will result in a disarm via the sanity check system, with delay if switch induced
704 // alternative is to prevent the rescue by returning to IDLE, but this could cause flyaways
705 } else if (rescueState.sensor.distanceToHomeM < gpsRescueConfig()->minRescueDth) {
706 if (rescueState.sensor.distanceToHomeM < 5.0f && rescueState.sensor.currentAltitudeCm < rescueState.intent.targetLandingAltitudeCm) {
707 // attempted initiation within 5m of home, and 'on the ground' -> instant disarm, for safety reasons
708 rescueState.phase = RESCUE_ABORT;
709 } else {
710 // Otherwise, attempted initiation inside minimum activation distance, at any height -> landing mode
711 rescueState.intent.altitudeStep = -rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
712 rescueState.intent.targetVelocityCmS = 0; // zero forward velocity
713 rescueState.intent.pitchAngleLimitDeg = 0; // flat on pitch
714 rescueState.intent.rollAngleLimitDeg = 0.0f; // flat on roll also
715 rescueState.intent.proximityToLandingArea = 0.0f; // force velocity iTerm to zero
716 rescueState.intent.targetAltitudeCm = rescueState.sensor.currentAltitudeCm + rescueState.intent.altitudeStep;
717 rescueState.phase = RESCUE_LANDING;
718 // start landing from current altitude
720 } else {
721 rescueState.phase = RESCUE_ATTAIN_ALT;
722 rescueState.intent.secondsFailing = 0; // reset the sanity check timer for the climb
723 initialAltitudeLow = (rescueState.sensor.currentAltitudeCm < rescueState.intent.returnAltitudeCm);
724 rescueState.intent.yawAttenuator = 0.0f;
725 rescueState.intent.targetVelocityCmS = rescueState.sensor.velocityToHomeCmS;
726 rescueState.intent.pitchAngleLimitDeg = 0.0f; // no pitch
727 rescueState.intent.rollAngleLimitDeg = 0.0f; // no roll until flying home
728 rescueState.intent.altitudeStep = 0.0f;
729 rescueState.intent.descentRateModifier = 0.0f;
730 rescueState.intent.velocityPidCutoffModifier = 1.0f; // normal cutoff until descending when increases 150->250% during descent
731 rescueState.intent.proximityToLandingArea = 0.0f; // force velocity iTerm to zero
732 rescueState.intent.velocityItermRelax = 0.0f; // and don't accumulate any
734 break;
736 case RESCUE_ATTAIN_ALT:
737 // gradually increment the target altitude until the craft reaches target altitude
738 // note that this can mean the target altitude may increase above returnAltitude if the craft lags target
739 // sanity check will abort if altitude gain is blocked for a cumulative period
740 rescueState.intent.altitudeStep = ((initialAltitudeLow) ? gpsRescueConfig()->ascendRate : -1.0f * gpsRescueConfig()->descendRate) * rescueState.sensor.gpsRescueTaskIntervalSeconds;
741 const bool currentAltitudeLow = rescueState.sensor.currentAltitudeCm < rescueState.intent.returnAltitudeCm;
742 if (initialAltitudeLow == currentAltitudeLow) {
743 // we started low, and still are low; also true if we started high, and still are too high
744 rescueState.intent.targetAltitudeCm += rescueState.intent.altitudeStep;
745 } else {
746 // target altitude achieved - move on to ROTATE phase, returning at target altitude
747 rescueState.intent.targetAltitudeCm = rescueState.intent.returnAltitudeCm;
748 rescueState.intent.altitudeStep = 0.0f;
749 rescueState.phase = RESCUE_ROTATE;
752 rescueState.intent.targetVelocityCmS = rescueState.sensor.velocityToHomeCmS;
753 // gives velocity P and I no error that otherwise would be present due to velocity drift at the start of the rescue
754 break;
756 case RESCUE_ROTATE:
757 if (rescueState.intent.yawAttenuator < 1.0f) { // acquire yaw authority over one second
758 rescueState.intent.yawAttenuator += rescueState.sensor.gpsRescueTaskIntervalSeconds;
760 if (rescueState.sensor.absErrorAngle < 30.0f) {
761 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->maxRescueAngle; // allow pitch
762 rescueState.phase = RESCUE_FLY_HOME; // enter fly home phase
763 rescueState.intent.secondsFailing = 0; // reset sanity timer for flight home
764 rescueState.intent.proximityToLandingArea = 1.0f; // velocity iTerm activated, initialise proximity for descent phase at 1.0
766 initialVelocityLow = rescueState.sensor.velocityToHomeCmS < gpsRescueConfig()->rescueGroundspeed; // used to set direction of velocity target change
767 rescueState.intent.targetVelocityCmS = rescueState.sensor.velocityToHomeCmS;
768 break;
770 case RESCUE_FLY_HOME:
771 if (rescueState.intent.yawAttenuator < 1.0f) { // be sure to accumulate full yaw authority
772 rescueState.intent.yawAttenuator += rescueState.sensor.gpsRescueTaskIntervalSeconds;
775 // velocity PIDs are now active
776 // update target velocity gradually, aiming for rescueGroundspeed with a time constant of 1.0s
777 const float targetVelocityError = gpsRescueConfig()->rescueGroundspeed - rescueState.intent.targetVelocityCmS;
778 const float velocityTargetStep = rescueState.sensor.gpsRescueTaskIntervalSeconds * targetVelocityError;
779 // velocityTargetStep is positive when starting low, negative when starting high
780 const bool targetVelocityIsLow = rescueState.intent.targetVelocityCmS < gpsRescueConfig()->rescueGroundspeed;
781 if (initialVelocityLow == targetVelocityIsLow) {
782 // also true if started faster than target velocity and target is still high
783 rescueState.intent.targetVelocityCmS += velocityTargetStep;
786 rescueState.intent.velocityItermRelax += 0.5f * rescueState.sensor.gpsRescueTaskIntervalSeconds * (1.0f - rescueState.intent.velocityItermRelax);
787 // slowly introduce velocity iTerm accumulation at start, goes 0 ->1 with time constant 2.0s
788 // there is always a lot of lag at the start
790 rescueState.intent.velocityPidCutoffModifier = 2.0f - rescueState.intent.velocityItermRelax;
791 // higher velocity cutoff for initial few seconds to improve accuracy; can be smoother later
793 rescueState.intent.rollAngleLimitDeg = 0.5f * rescueState.intent.velocityItermRelax * gpsRescueConfig()->maxRescueAngle;
794 // gradually gain roll capability to max of half of max pitch angle
796 if (newGPSData) {
797 if (rescueState.sensor.distanceToHomeM <= rescueState.intent.descentDistanceM) {
798 rescueState.phase = RESCUE_DESCENT;
799 rescueState.intent.secondsFailing = 0; // reset sanity timer for descent
802 break;
804 case RESCUE_DESCENT:
805 // attenuate velocity and altitude targets while updating the heading to home
806 if (rescueState.sensor.currentAltitudeCm < rescueState.intent.targetLandingAltitudeCm) {
807 // enter landing mode once below landing altitude
808 rescueState.phase = RESCUE_LANDING;
809 rescueState.intent.secondsFailing = 0; // reset sanity timer for landing
811 descend();
812 break;
814 case RESCUE_LANDING:
815 // Reduce altitude target steadily until impact, then disarm.
816 // control yaw angle and throttle and pitch, attenuate velocity, roll and pitch iTerm
817 // increase velocity smoothing cutoff as we get closer to ground
818 descend();
819 disarmOnImpact();
820 break;
822 case RESCUE_COMPLETE:
823 rescueStop();
824 break;
826 case RESCUE_ABORT:
827 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
828 disarm(DISARM_REASON_FAILSAFE);
829 rescueState.intent.secondsFailing = 0; // reset sanity timers so we can re-arm
830 rescueStop();
831 break;
833 case RESCUE_DO_NOTHING:
834 disarmOnImpact();
835 break;
837 default:
838 break;
841 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 3, lrintf(rescueState.intent.targetAltitudeCm));
842 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 3, lrintf(rescueState.intent.targetAltitudeCm));
843 DEBUG_SET(DEBUG_RTH, 0, lrintf(rescueState.intent.maxAltitudeCm));
845 performSanityChecks();
846 rescueAttainPosition();
848 newGPSData = false;
851 float gpsRescueGetYawRate(void)
853 return rescueYaw;
856 float gpsRescueGetThrottle(void)
858 // Calculated a desired commanded throttle scaled from 0.0 to 1.0 for use in the mixer.
859 // We need to compensate for min_check since the throttle value set by gps rescue
860 // is based on the raw rcCommand value commanded by the pilot.
861 float commandedThrottle = scaleRangef(rescueThrottle, MAX(rxConfig()->mincheck, PWM_RANGE_MIN), PWM_RANGE_MAX, 0.0f, 1.0f);
862 commandedThrottle = constrainf(commandedThrottle, 0.0f, 1.0f);
864 return commandedThrottle;
867 bool gpsRescueIsConfigured(void)
869 return failsafeConfig()->failsafe_procedure == FAILSAFE_PROCEDURE_GPS_RESCUE || isModeActivationConditionPresent(BOXGPSRESCUE);
872 bool gpsRescueIsAvailable(void)
874 return rescueState.isAvailable;
877 bool gpsRescueIsDisabled(void)
878 // used for OSD warning
880 return (!STATE(GPS_FIX_HOME));
883 #ifdef USE_MAG
884 bool gpsRescueDisableMag(void)
886 return ((!gpsRescueConfig()->useMag || magForceDisable) && (rescueState.phase >= RESCUE_INITIALIZE) && (rescueState.phase <= RESCUE_LANDING));
888 #endif
889 #endif