Improve landing, update OSD, BB names, smooth Pitch, sanity bugfix
[betaflight.git] / src / main / flight / gps_rescue.c
blob2e4c90e958d611deb45c9b6fea873c47d0f26b33
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 <math.h>
21 #include "platform.h"
23 #ifdef USE_GPS_RESCUE
25 #include "build/debug.h"
27 #include "common/axis.h"
28 #include "common/maths.h"
29 #include "common/utils.h"
31 #include "drivers/time.h"
33 #include "io/gps.h"
35 #include "config/config.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/failsafe.h"
42 #include "flight/imu.h"
43 #include "flight/pid.h"
44 #include "flight/position.h"
46 #include "pg/pg.h"
47 #include "pg/pg_ids.h"
49 #include "rx/rx.h"
51 #include "sensors/acceleration.h"
53 #include "gps_rescue.h"
55 typedef enum {
56 RESCUE_SANITY_OFF = 0,
57 RESCUE_SANITY_ON,
58 RESCUE_SANITY_FS_ONLY
59 } gpsRescueSanity_e;
61 typedef enum {
62 RESCUE_IDLE,
63 RESCUE_INITIALIZE,
64 RESCUE_ATTAIN_ALT,
65 RESCUE_ROTATE,
66 RESCUE_FLY_HOME,
67 RESCUE_DESCENT,
68 RESCUE_LANDING,
69 RESCUE_ABORT,
70 RESCUE_COMPLETE,
71 RESCUE_DO_NOTHING
72 } rescuePhase_e;
74 typedef enum {
75 RESCUE_HEALTHY,
76 RESCUE_FLYAWAY,
77 RESCUE_GPSLOST,
78 RESCUE_LOWSATS,
79 RESCUE_CRASH_FLIP_DETECTED,
80 RESCUE_STALLED,
81 RESCUE_TOO_CLOSE,
82 RESCUE_NO_HOME_POINT
83 } rescueFailureState_e;
85 typedef struct {
86 float returnAltitudeCm;
87 float targetAltitudeCm;
88 float targetLandingAltitudeCm;
89 uint16_t targetVelocityCmS;
90 uint8_t pitchAngleLimitDeg;
91 int8_t rollAngleLimitDeg; // must have a negative to positive range
92 bool updateYaw;
93 float descentDistanceM;
94 int8_t secondsFailing;
95 float altitudeStep;
96 float descentSpeedModifier;
97 float yawAttenuator;
98 } rescueIntent_s;
100 typedef struct {
101 float maxAltitudeCm;
102 float currentAltitudeCm;
103 float distanceToHomeCm;
104 float distanceToHomeM;
105 uint16_t groundSpeedCmS; //cm/s
106 int16_t directionToHome;
107 float accMagnitude;
108 bool healthy;
109 float errorAngle;
110 float gpsDataIntervalSeconds;
111 float altitudeDataIntervalSeconds;
112 float velocityToHomeCmS;
113 float alitutudeStepCm;
114 float maxPitchStep;
115 float filterK;
116 float absErrorAngle;
117 } rescueSensorData_s;
119 typedef struct {
120 rescuePhase_e phase;
121 rescueFailureState_e failure;
122 rescueSensorData_s sensor;
123 rescueIntent_s intent;
124 bool isAvailable;
125 } rescueState_s;
127 typedef enum {
128 MAX_ALT,
129 FIXED_ALT,
130 CURRENT_ALT
131 } altitudeMode_e;
133 #define GPS_RESCUE_MAX_YAW_RATE 180 // deg/sec max yaw rate
134 #define GPS_RESCUE_MIN_DESCENT_DIST_M 5 // minimum descent distance allowed
135 #define GPS_RESCUE_MAX_ITERM_VELOCITY 1000 // max allowed iterm value for velocity
136 #define GPS_RESCUE_MAX_ITERM_THROTTLE 200 // max allowed iterm value for throttle
137 #define GPS_RESCUE_MAX_PITCH_RATE 3000 // max allowed change in pitch per second in degrees * 100
139 #ifdef USE_MAG
140 #define GPS_RESCUE_USE_MAG true
141 #else
142 #define GPS_RESCUE_USE_MAG false
143 #endif
145 PG_REGISTER_WITH_RESET_TEMPLATE(gpsRescueConfig_t, gpsRescueConfig, PG_GPS_RESCUE, 3);
147 PG_RESET_TEMPLATE(gpsRescueConfig_t, gpsRescueConfig,
148 .minRescueDth = 30,
149 .altitudeMode = MAX_ALT,
150 .rescueAltitudeBufferM = 10,
151 .ascendRate = 500, // cm/s, for altitude corrections on ascent
153 .initialAltitudeM = 30,
154 .rescueGroundspeed = 500,
155 .angle = 40,
156 .rollMix = 100,
158 .descentDistanceM = 20,
159 .descendRate = 100, // cm/s, minimum for descent and landing phase, or for descending if starting high ascent
160 .targetLandingAltitudeM = 5,
162 .throttleMin = 1100,
163 .throttleMax = 1600,
164 .throttleHover = 1275,
166 .allowArmingWithoutFix = false,
167 .sanityChecks = RESCUE_SANITY_FS_ONLY,
169 .throttleP = 15,
170 .throttleI = 15,
171 .throttleD = 15,
172 .velP = 6,
173 .velI = 20,
174 .velD = 50,
175 .yawP = 25,
177 .useMag = GPS_RESCUE_USE_MAG
180 static float rescueThrottle;
181 static float rescueYaw;
182 float gpsRescueAngle[ANGLE_INDEX_COUNT] = { 0, 0 };
183 bool magForceDisable = false;
184 static bool newGPSData = false;
185 static pt2Filter_t throttleDLpf;
186 static pt3Filter_t pitchLpf;
188 rescueState_s rescueState;
190 void gpsRescueInit(void)
192 const float sampleTimeS = HZ_TO_INTERVAL(TASK_ALTITUDE_RATE_HZ);
194 const float throttleDCutoffHz = positionConfig()->altitude_d_lpf / 100.0f;
195 const float throttleDCutoffGain = pt2FilterGain(throttleDCutoffHz, sampleTimeS);
196 pt2FilterInit(&throttleDLpf, throttleDCutoffGain);
198 const float pitchCutoffHz = 4.0f;
199 const float pitchCutoffGain = pt3FilterGain(pitchCutoffHz, sampleTimeS);
200 pt3FilterInit(&pitchLpf, pitchCutoffGain);
205 If we have new GPS data, update home heading if possible and applicable.
207 void rescueNewGpsData(void)
209 newGPSData = true;
212 static void rescueStart()
214 rescueState.phase = RESCUE_INITIALIZE;
217 static void rescueStop()
219 rescueState.phase = RESCUE_IDLE;
222 // Things that need to run when GPS Rescue is enabled, and while armed, but while there is no Rescue in place
223 static void idleTasks()
225 // Don't calculate these values while disarmed
226 if (!ARMING_FLAG(ARMED)) {
227 rescueState.sensor.maxAltitudeCm = 0;
228 return;
231 // Don't update any altitude related stuff if we haven't applied a proper altitude offset
232 if (!isAltitudeOffset()) {
233 return;
236 // Store the max altitude we see not during RTH so we know our fly-back minimum alt
237 rescueState.sensor.maxAltitudeCm = MAX(rescueState.sensor.currentAltitudeCm, rescueState.sensor.maxAltitudeCm);
239 if (newGPSData) {
240 // set the target altitude and velocity to current values, so there will be no D kick on first run
241 rescueState.intent.targetAltitudeCm = rescueState.sensor.currentAltitudeCm;
242 // Keep the descent distance and intended altitude up to date with latest GPS values
243 rescueState.intent.descentDistanceM = constrainf(rescueState.sensor.distanceToHomeM, GPS_RESCUE_MIN_DESCENT_DIST_M, gpsRescueConfig()->descentDistanceM);
244 const float initialAltitudeCm = gpsRescueConfig()->initialAltitudeM * 100.0f;
245 const float rescueAltitudeBufferCm = gpsRescueConfig()->rescueAltitudeBufferM * 100.0f;
246 switch (gpsRescueConfig()->altitudeMode) {
247 case FIXED_ALT:
248 rescueState.intent.returnAltitudeCm = initialAltitudeCm;
249 break;
250 case CURRENT_ALT:
251 rescueState.intent.returnAltitudeCm = rescueState.sensor.currentAltitudeCm + rescueAltitudeBufferCm;
252 break;
253 case MAX_ALT:
254 default:
255 rescueState.intent.returnAltitudeCm = rescueState.sensor.maxAltitudeCm + rescueAltitudeBufferCm;
256 break;
261 static void rescueAttainPosition()
263 // runs at 100hz, but only updates RPYT settings when new GPS Data arrives and when not in idle phase.
264 static float previousVelocityError = 0.0f;
265 static float velocityI = 0.0f;
266 static float previousVelocityD = 0.0f; // for smoothing
267 static float previousPitchAdjustment = 0.0f;
268 static float throttleI = 0.0f;
269 static float previousAltitudeError = 0.0f;
270 static int16_t throttleAdjustment = 0;
272 switch (rescueState.phase) {
273 case RESCUE_IDLE:
274 // values to be returned when no rescue is active
275 gpsRescueAngle[AI_PITCH] = 0.0f;
276 gpsRescueAngle[AI_ROLL] = 0.0f;
277 rescueThrottle = rcCommand[THROTTLE];
278 return;
279 case RESCUE_INITIALIZE:
280 // Initialize internal variables each time GPS Rescue is started
281 // Note that sensor values can't be initialised here. Use idleTasks() to initialise them.
282 previousVelocityError = 0.0f;
283 velocityI = 0.0f;
284 previousVelocityD = 0.0f;
285 previousPitchAdjustment = 0.0f;
286 throttleI = 0.0f;
287 previousAltitudeError = 0.0f;
288 throttleAdjustment = 0;
289 return;
290 case RESCUE_DO_NOTHING:
291 gpsRescueAngle[AI_PITCH] = 0.0f;
292 gpsRescueAngle[AI_ROLL] = 0.0f;
293 rescueThrottle = gpsRescueConfig()->throttleHover - 50;
294 return;
295 default:
296 break;
300 Altitude (throttle) controller
302 // currentAltitudeCm is updated at TASK_GPS_RATE since GPS initiates updateGPSRescueState()
303 const float altitudeError = (rescueState.intent.targetAltitudeCm - rescueState.sensor.currentAltitudeCm) * 0.01f;
304 // height above target in metres (negative means too low)
305 // at the start, the target starts at current altitude plus one step. Increases stepwise to intended value.
307 // P component
308 const float throttleP = gpsRescueConfig()->throttleP * altitudeError;
310 // I component
311 throttleI += 0.1f * gpsRescueConfig()->throttleI * altitudeError * rescueState.sensor.altitudeDataIntervalSeconds;
312 throttleI = constrainf(throttleI, -1.0f * GPS_RESCUE_MAX_ITERM_THROTTLE, 1.0f * GPS_RESCUE_MAX_ITERM_THROTTLE);
313 // up to 20% increase in throttle from I alone
315 // D component is error based, so includes positive boost when climbing and negative boost on descent
316 float throttleDRaw = ((altitudeError - previousAltitudeError) / rescueState.sensor.altitudeDataIntervalSeconds);
317 previousAltitudeError = altitudeError;
318 throttleDRaw += rescueState.intent.descentSpeedModifier * throttleDRaw;
319 // add up to 2x D when descent rate is faster
321 float throttleD = pt2FilterApply(&throttleDLpf, throttleDRaw);
322 throttleD = gpsRescueConfig()->throttleD * throttleD;
324 // acceleration component not currently implemented - was needed previously due to GPS lag, maybe not needed now.
326 float tiltAdjustment = 1.0f - getCosTiltAngle(); // 0 = flat, gets to 0.2 correcting on a windy day
327 tiltAdjustment *= (gpsRescueConfig()->throttleHover - 1000);
328 // if hover is 1300, and adjustment .2, this gives us 0.2*300 or 60 of extra throttle, not much, but useful
329 // too much and landings with lots of pitch adjustment, eg windy days, can be a problem
331 throttleAdjustment = throttleP + throttleI + throttleD + tiltAdjustment;
333 rescueThrottle = gpsRescueConfig()->throttleHover + throttleAdjustment;
334 rescueThrottle = constrainf(rescueThrottle, gpsRescueConfig()->throttleMin, gpsRescueConfig()->throttleMax);
335 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 0, throttleP);
336 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 1, throttleD);
339 Heading / yaw controller
341 // directionToHome and distanceToHome are accurate if the GPS Home point is accurate.
342 // attitude.values.yaw is set by imuCalculateEstimatedAttitude() but only updated while groundspeed exceeds 2 m/s
343 // for accurate return, the craft should exceed 5m/s in clean nose-forward flight at some point
344 // the faster the return speed, the more accurate the IMU will be, but the consequences of IMU error at the start are greater
345 // A compass (magnetometer) is vital for accurate GPS rescue at slow speeds
347 // if the quad is pointing 180 degrees wrong at failsafe time, it will take 2s to rotate fully at 90 deg/s max rate
348 // this gives the level mode controller time to adjust pitch and roll during the yaw
349 // we need a relatively gradual trajectory change for attitude.values.yaw to update effectively
351 if (rescueState.intent.updateYaw) {
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 - ABS(rescueYaw) * 0.01f, 0.0f, 1.0f);
357 // attenuate roll as yaw rate increases, no roll at 100 deg/s of yaw
358 const float rollAdjustment = - rescueYaw * gpsRescueConfig()->rollMix * rollMixAttenuator;
359 // mix in the desired amount of roll; 1:1 yaw:roll when rollMix = 100 and yaw angles are small
360 // when gpsRescueConfig()->rollMix is zero, there is no roll adjustment
361 // rollAdjustment is degrees * 100
362 // note that the roll element has the same sign as the yaw element *before* GET_DIRECTION
363 gpsRescueAngle[AI_ROLL] = constrainf(rollAdjustment, -rescueState.intent.rollAngleLimitDeg * 100.0f, rescueState.intent.rollAngleLimitDeg * 100.0f);
364 // gpsRescueAngle is added to the normal roll Angle Mode corrections in pid.c
366 rescueYaw *= GET_DIRECTION(rcControlsConfig()->yaw_control_reversed);
368 } else {
369 rescueYaw = 0.0f;
373 Pitch / velocity controller
375 static float pitchAdjustment = 0.0f;
376 static float pitchAdjustmentFiltered = 0.0f;
377 if (newGPSData) {
379 const float sampleIntervalNormaliseFactor = rescueState.sensor.gpsDataIntervalSeconds * 10.0f;
380 const float velocityTargetLimiter = constrainf((60.0f -rescueState.sensor.absErrorAngle) / 60.0f, 0.0f, 1.0f);
381 // attenuate velocity target when quad is not pointing towards home
382 // stops attempting to gain velocity when pointing the wrong way, eg overshooting home point (sudden heading error)
383 float velocityError = (rescueState.intent.targetVelocityCmS * velocityTargetLimiter - rescueState.sensor.velocityToHomeCmS);
385 // velocityError is in cm per second, positive means too slow.
386 // NB positive pitch setpoint means nose down.
387 // quite heavily smoothed
388 // IdleTasks sets target velocity to current velocity to minimise D spike when starting, and keep error = 0
390 // P component
391 const float velocityP = velocityError * gpsRescueConfig()->velP;
393 // I component
394 velocityI += 0.01f * gpsRescueConfig()->velI * velocityError * sampleIntervalNormaliseFactor;
395 // normalisation increases amount added when data rate is slower than expected
396 velocityI *= rescueState.intent.targetVelocityCmS / rescueState.intent.targetVelocityCmS;
397 // attenuate iTerm at slower target velocity, to minimise overshoot, mostly during deceleration to landing phase
398 velocityI = constrainf(velocityI, -1.0f * GPS_RESCUE_MAX_ITERM_VELOCITY, 1.0f * GPS_RESCUE_MAX_ITERM_VELOCITY);
399 // I component alone cannot exceed a pitch angle of 10%
401 // D component
402 float velocityD = ((velocityError - previousVelocityError) / sampleIntervalNormaliseFactor);
403 previousVelocityError = velocityError;
404 // simple first order filter on derivative with k = 0.5 for 200ms steps
405 velocityD = previousVelocityD + rescueState.sensor.filterK * (velocityD - previousVelocityD);
406 previousVelocityD = velocityD;
407 velocityD *= gpsRescueConfig()->velD;
409 // Pitch PIDsum and smoothing
410 pitchAdjustment = velocityP + velocityD + velocityI;
411 // simple rate of change limiter - not more than 25 deg/s of pitch change to keep pitch smooth
412 float pitchAdjustmentDelta = pitchAdjustment - previousPitchAdjustment;
413 if (pitchAdjustmentDelta > rescueState.sensor.maxPitchStep) {
414 pitchAdjustment = previousPitchAdjustment + rescueState.sensor.maxPitchStep;
415 } else if (pitchAdjustmentDelta < -rescueState.sensor.maxPitchStep) {
416 pitchAdjustment = previousPitchAdjustment - rescueState.sensor.maxPitchStep;
418 const float movingAvgPitchAdjustment = 0.5f * (previousPitchAdjustment + pitchAdjustment);
419 // moving average seems to work best here, a lot of sequential up and down in velocity data
420 previousPitchAdjustment = pitchAdjustment;
421 pitchAdjustment = movingAvgPitchAdjustment;
422 // pitchAdjustment is the absolute Pitch angle adjustment value in degrees * 100
423 // it gets added to the normal level mode Pitch adjustments in pid.c
425 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 0, velocityP);
426 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 1, velocityD);
427 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 3, rescueState.intent.targetVelocityCmS);
428 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 1, rescueState.intent.targetVelocityCmS);
431 pitchAdjustmentFiltered = pt3FilterApply(&pitchLpf, pitchAdjustment);
432 // upsampling smoothing of steps in pitch angle
434 gpsRescueAngle[AI_PITCH] = constrainf(pitchAdjustmentFiltered, -rescueState.intent.pitchAngleLimitDeg * 100.0f, rescueState.intent.pitchAngleLimitDeg * 100.0f);
435 // this angle gets added to the normal pitch Angle Mode control values in pid.c - will be seen in pitch setpoint
437 DEBUG_SET(DEBUG_RTH, 0, gpsRescueAngle[AI_PITCH]);
441 static void performSanityChecks()
443 static timeUs_t previousTimeUs = 0; // Last time Stalled/LowSat was checked
444 static float prevAltitudeCm = 0.0f; // to calculate ascent or descent change
445 static float prevTargetAltitudeCm = 0.0f; // to calculate ascent or descent target change
446 static int8_t secondsLowSats = 0; // Minimum sat detection
447 static int8_t secondsDoingNothing = 0; // Limit on doing nothing
448 const timeUs_t currentTimeUs = micros();
450 if (rescueState.phase == RESCUE_IDLE) {
451 rescueState.failure = RESCUE_HEALTHY;
452 return;
453 } else if (rescueState.phase == RESCUE_INITIALIZE) {
454 // Initialize internal variables each time GPS Rescue is started
455 previousTimeUs = currentTimeUs;
456 prevAltitudeCm = rescueState.sensor.currentAltitudeCm;
457 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
458 secondsLowSats = 5; // Start the count at 5 to be less forgiving at the beginning
459 secondsDoingNothing = 0;
460 return;
463 // Handle rescue failures. Don't disarm for rescue failure during stick induced rescues.
464 const bool hardFailsafe = !rxIsReceivingSignal();
465 if (rescueState.failure != RESCUE_HEALTHY) {
466 if (gpsRescueConfig()->sanityChecks == RESCUE_SANITY_ON) {
467 rescueState.phase = RESCUE_ABORT;
468 } else if ((gpsRescueConfig()->sanityChecks == RESCUE_SANITY_FS_ONLY) && hardFailsafe) {
469 rescueState.phase = RESCUE_ABORT;
470 } else {
471 rescueState.phase = RESCUE_DO_NOTHING;
475 // If crash recovery is triggered during a rescue, immediately disarm, ignoring the crash flip recovery settings.
476 if (crashRecoveryModeActive()) {
477 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
478 disarm(DISARM_REASON_CRASH_PROTECTION);
479 rescueStop();
482 // Check if GPS comms are healthy
483 if (!rescueState.sensor.healthy) {
484 rescueState.failure = RESCUE_GPSLOST;
486 // Things that should run at a low refresh rate (such as flyaway detection, etc)
487 // This runs at 1hz
488 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
489 if (dTime < 1000000) { //1hz
490 return;
492 previousTimeUs = currentTimeUs;
494 if (rescueState.phase == RESCUE_FLY_HOME) {
495 rescueState.intent.secondsFailing += (rescueState.sensor.velocityToHomeCmS < 0.5 * rescueState.intent.targetVelocityCmS) ? 1 : -1;
496 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 20);
497 if (rescueState.intent.secondsFailing == 20) {
498 #ifdef USE_MAG
499 //If there is a mag and has not been disabled, we have to assume is healthy and has been used in imu.c
500 if (sensors(SENSOR_MAG) && gpsRescueConfig()->useMag && !magForceDisable) {
501 //Try again with mag disabled
502 magForceDisable = true;
503 rescueState.intent.secondsFailing = 0;
504 } else
505 #endif
507 rescueState.failure = RESCUE_STALLED;
512 // These conditions are 'special', in that even with sanity checks off, they should still apply
513 const float actualAltitudeChange = rescueState.sensor.currentAltitudeCm - prevAltitudeCm;
514 const float targetAltitudeChange = rescueState.intent.targetAltitudeCm - prevTargetAltitudeCm;
515 const float ratio = actualAltitudeChange / targetAltitudeChange;
516 if (rescueState.phase == RESCUE_ATTAIN_ALT) {
517 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
518 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
519 if (rescueState.intent.secondsFailing == 10) {
521 rescueState.phase = RESCUE_ABORT;
522 // if stuck in a tree while climbing, or otherwise can't climb, stop motors and disarm
525 } else if (rescueState.phase == RESCUE_LANDING || rescueState.phase == RESCUE_DESCENT) {
526 rescueState.intent.secondsFailing += ratio > 0.5f ? -1 : 1;
527 rescueState.intent.secondsFailing = constrain(rescueState.intent.secondsFailing, 0, 10);
528 if (rescueState.intent.secondsFailing == 10) {
530 rescueState.phase = RESCUE_ABORT;
531 // if stuck in a tree while climbing, or don't disarm on impact, or enable GPS rescue on the ground too close
534 } else if (rescueState.phase == RESCUE_DO_NOTHING) {
535 secondsDoingNothing = MIN(secondsDoingNothing + 1, 10);
536 if (secondsDoingNothing == 10) {
537 rescueState.phase = RESCUE_ABORT;
538 // prevent indefinite flyaways when sanity checks are off, and
539 // time limit the "do nothing" period when a switch initiated failsafe fails sanity checks
540 // this is controversial
543 prevAltitudeCm = rescueState.sensor.currentAltitudeCm;
544 prevTargetAltitudeCm = rescueState.intent.targetAltitudeCm;
546 secondsLowSats += gpsSol.numSat < (gpsConfig()->gpsMinimumSats) ? 1 : -1;
547 secondsLowSats = constrain(secondsLowSats, 0, 10);
549 if (secondsLowSats == 10) {
550 rescueState.failure = RESCUE_LOWSATS;
553 DEBUG_SET(DEBUG_RTH, 2, rescueState.failure);
554 DEBUG_SET(DEBUG_RTH, 3, (rescueState.intent.secondsFailing * 100 + secondsLowSats)); //Failure can change with no new GPS Data
557 static void sensorUpdate()
559 static float prevDistanceToHomeCm = 0.0f;
560 const timeUs_t currentTimeUs = micros();
562 static timeUs_t previousAltitudeDataTimeUs = 0;
563 const timeDelta_t altitudeDataIntervalUs = cmpTimeUs(currentTimeUs, previousAltitudeDataTimeUs);
564 rescueState.sensor.altitudeDataIntervalSeconds = altitudeDataIntervalUs * 0.000001f;
565 previousAltitudeDataTimeUs = currentTimeUs;
567 rescueState.sensor.currentAltitudeCm = getAltitude();
569 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 2, rescueState.sensor.currentAltitudeCm);
570 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 2, rescueState.sensor.currentAltitudeCm);
571 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 0, rescueState.sensor.groundSpeedCmS); // groundspeed cm/s
572 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 1, gpsSol.groundCourse); // degrees * 10
573 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 2, attitude.values.yaw); // degrees * 10
574 DEBUG_SET(DEBUG_GPS_RESCUE_HEADING, 3, rescueState.sensor.directionToHome); // degrees * 10
576 rescueState.sensor.healthy = gpsIsHealthy();
578 if (rescueState.phase == RESCUE_LANDING) {
579 // do this at sensor update rate, not the much slower GPS rate, for quick disarm
580 rescueState.sensor.accMagnitude = (float) sqrtf(sq(acc.accADC[Z]) + sq(acc.accADC[X]) + sq(acc.accADC[Y])) * acc.dev.acc_1G_rec;
583 rescueState.sensor.directionToHome = GPS_directionToHome;
584 rescueState.sensor.errorAngle = (attitude.values.yaw - rescueState.sensor.directionToHome) * 0.1f;
585 // both attitude and direction are in degrees * 10, errorAngle is degrees
586 if (rescueState.sensor.errorAngle <= -180) {
587 rescueState.sensor.errorAngle += 360;
588 } else if (rescueState.sensor.errorAngle > 180) {
589 rescueState.sensor.errorAngle -= 360;
591 rescueState.sensor.absErrorAngle = fabsf(rescueState.sensor.errorAngle);
593 if (!newGPSData) {
594 return;
597 rescueState.sensor.distanceToHomeCm = GPS_distanceToHomeCm;
598 rescueState.sensor.distanceToHomeM = rescueState.sensor.distanceToHomeCm / 100.0f;
599 rescueState.sensor.groundSpeedCmS = gpsSol.groundSpeed; // cm/s
602 static timeUs_t previousGPSDataTimeUs = 0;
603 const timeDelta_t gpsDataIntervalUs = cmpTimeUs(currentTimeUs, previousGPSDataTimeUs);
604 rescueState.sensor.gpsDataIntervalSeconds = constrainf(gpsDataIntervalUs * 0.000001f, 0.01f, 1.0f);
605 // Range from 10ms (100hz) to 1000ms (1Hz). Intended to cover common GPS data rates and exclude unusual values.
606 previousGPSDataTimeUs = currentTimeUs;
608 rescueState.sensor.filterK = pt1FilterGain(0.8, rescueState.sensor.gpsDataIntervalSeconds);
609 // 0.8341 for 1hz, 0.5013 for 5hz, 0.3345 for 10hz, 0.1674 for 25Hz, etc
611 rescueState.sensor.velocityToHomeCmS = (prevDistanceToHomeCm - rescueState.sensor.distanceToHomeCm) / rescueState.sensor.gpsDataIntervalSeconds;
612 // positive = towards home. First value is useless since prevDistanceToHomeCm was zero.
613 prevDistanceToHomeCm = rescueState.sensor.distanceToHomeCm;
615 rescueState.sensor.maxPitchStep = rescueState.sensor.gpsDataIntervalSeconds * GPS_RESCUE_MAX_PITCH_RATE;
617 DEBUG_SET(DEBUG_GPS_RESCUE_VELOCITY, 2, rescueState.sensor.velocityToHomeCmS);
618 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 0, rescueState.sensor.velocityToHomeCmS);
622 // This function flashes "RESCUE N/A" in the OSD if:
623 // 1. sensor healthy - GPS data is being received.
624 // 2. GPS has a 3D fix.
625 // 3. GPS number of satellites is greater than or equal to the minimum configured satellite count.
626 // Note 1: cannot arm without the required number of sats;
627 // hence this flashing indicates that after having enough sats, we now have below the minimum and the rescue likely would fail
628 // Note 2: this function does not take into account the distance from homepoint etc. (gps_rescue_min_dth).
629 // The sanity checks are independent, this just provides the OSD warning
630 static bool checkGPSRescueIsAvailable(void)
632 static timeUs_t previousTimeUs = 0; // Last time LowSat was checked
633 const timeUs_t currentTimeUs = micros();
634 static int8_t secondsLowSats = 0; // Minimum sat detection
635 static bool lowsats = false;
636 static bool noGPSfix = false;
637 bool result = true;
639 if (!gpsIsHealthy() || !STATE(GPS_FIX_HOME)) {
640 return false;
643 // Things that should run at a low refresh rate >> ~1hz
644 const timeDelta_t dTime = cmpTimeUs(currentTimeUs, previousTimeUs);
645 if (dTime < 1000000) { //1hz
646 if (noGPSfix || lowsats) {
647 result = false;
649 return result;
652 previousTimeUs = currentTimeUs;
654 if (!STATE(GPS_FIX)) {
655 result = false;
656 noGPSfix = true;
657 } else {
658 noGPSfix = false;
661 secondsLowSats = constrain(secondsLowSats + ((gpsSol.numSat < gpsConfig()->gpsMinimumSats) ? 1 : -1), 0, 2);
662 if (secondsLowSats == 2) {
663 lowsats = true;
664 result = false;
665 } else {
666 lowsats = false;
669 return result;
672 void disarmOnImpact(void)
674 if (rescueState.sensor.accMagnitude > 3.0f) {
675 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
676 disarm(DISARM_REASON_GPS_RESCUE);
677 rescueStop();
681 void descend(void)
683 rescueState.intent.altitudeStep = -1.0f * rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
684 // adjust altitude step for interval between altitude readings
686 const float descentAttenuator = rescueState.intent.returnAltitudeCm / 2000.0f;
687 // descend more slowly if return altitude is lower than 20m
688 if (descentAttenuator < 1.0f) {
689 rescueState.intent.altitudeStep *= descentAttenuator;
692 rescueState.intent.descentSpeedModifier = constrainf(rescueState.sensor.currentAltitudeCm / 4000.0f, 0.0f, 1.0f);
693 rescueState.intent.targetAltitudeCm += rescueState.intent.altitudeStep * (1.0f + (2.0f * rescueState.intent.descentSpeedModifier));
694 // increase descent rate to max of 3x default above 40m, 2x above 20m, 1.166 at 5m, default at ground level.
697 void altitudeAchieved(void)
699 rescueState.intent.targetAltitudeCm = rescueState.intent.returnAltitudeCm;
700 rescueState.intent.altitudeStep = 0;
701 rescueState.intent.updateYaw = true; // allow yaw updating
702 rescueState.phase = RESCUE_ROTATE;
705 void updateGPSRescueState(void)
706 // this runs a lot faster than the GPS Data update rate, and runs whether or not rescue is active
708 if (!FLIGHT_MODE(GPS_RESCUE_MODE)) {
709 rescueStop(); // sets phase to idle; does nothing else. Idle tasks still run.
710 } else if (FLIGHT_MODE(GPS_RESCUE_MODE) && rescueState.phase == RESCUE_IDLE) {
711 rescueStart(); // sets phase to rescue_initialise if we enter GPS Rescue mode while idle
712 rescueAttainPosition(); // Initialise basic parameters when a Rescue starts (can't initialise sensor data reliably)
713 performSanityChecks(); // Initialises sanity check values when a Rescue starts
716 // Will now be in RESCUE_INITIALIZE mode, if just entered Rescue while IDLE, otherwise stays IDLE
718 sensorUpdate(); // always get latest GPS / Altitude data; update ascend and descend rates
720 uint8_t halfAngle = gpsRescueConfig()->angle / 2;
721 float proximityToLandingArea = 0.0f;
722 bool startedLow = true;
723 rescueState.isAvailable = checkGPSRescueIsAvailable();
726 switch (rescueState.phase) {
727 case RESCUE_IDLE:
728 // in Idle phase = NOT in GPS Rescue
729 // set maxAltitude for flight
730 // update the return altitude and descent distance values, to have valid settings immediately they are needed
731 // initialise the target altitude and velocity to current values to minimise D spike on startup
732 idleTasks();
733 break;
734 // sanity checks are bypassed in IDLE mode; instead, failure state is always initialised to HEALTHY
735 // target altitude is always set to current altitude.
737 case RESCUE_INITIALIZE:
738 // Things that should abort the start of a Rescue
739 if (!STATE(GPS_FIX_HOME)) {
740 // we didn't get a home point on arming
741 rescueState.failure = RESCUE_NO_HOME_POINT;
742 // will result in a disarm via the sanity check system, with delay if switch induced
743 // alternative is to prevent the rescue by returning to IDLE, but this could cause flyaways
744 } else if (rescueState.sensor.distanceToHomeM < gpsRescueConfig()->minRescueDth) {
745 // Attempt to initiate inside minimum activation distance -> landing mode
746 rescueState.intent.altitudeStep = -rescueState.sensor.altitudeDataIntervalSeconds * gpsRescueConfig()->descendRate;
747 rescueState.intent.targetVelocityCmS = 0; // zero forward velocity
748 rescueState.intent.pitchAngleLimitDeg = 0; // flat on pitch
749 rescueState.intent.rollAngleLimitDeg = 0; // flat on roll also
750 rescueState.intent.targetAltitudeCm = rescueState.sensor.currentAltitudeCm + rescueState.intent.altitudeStep;
751 rescueState.phase = RESCUE_LANDING;
752 // start landing from current altitude
753 } else {
754 rescueState.phase = RESCUE_ATTAIN_ALT;
755 rescueState.intent.secondsFailing = 0; // reset the sanity check timer for the climb
756 rescueState.intent.targetLandingAltitudeCm = 100.0f * gpsRescueConfig()->targetLandingAltitudeM;
757 startedLow = (rescueState.sensor.currentAltitudeCm <= rescueState.intent.returnAltitudeCm);
758 rescueState.intent.updateYaw = false; // point the nose to home at all times during the rescue
759 rescueState.intent.targetVelocityCmS = 0; // zero forward velocity while climbing
760 rescueState.intent.pitchAngleLimitDeg = 0; // flat on pitch
761 rescueState.intent.rollAngleLimitDeg = 0; // flat on roll also
762 rescueState.intent.altitudeStep = 0;
763 rescueState.intent.descentSpeedModifier = 0.0f;
764 rescueState.intent.yawAttenuator = 0.0f;
766 break;
768 case RESCUE_ATTAIN_ALT:
769 // gradually increment the target altitude until the final target altitude value is set
770 // also require that the final target altitude has been achieved before moving on
771 // sanity check will abort if altitude gain is blocked for a cumulative period
772 // TO DO: if overshoots are a problem after craft achieves target altitude changes, adjust termination threshold with current vertical velocity
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 // gradually acquire yaw and roll authority over 50 iterations, or about half a second
791 if (rescueState.intent.yawAttenuator < 1.0f) {
792 rescueState.intent.yawAttenuator += 0.02f;
795 if (rescueState.sensor.absErrorAngle < 90.0f) {
796 const float angleToHome = 1.0f - (rescueState.sensor.absErrorAngle / 90.0f);
797 // once , gradually increase target velocity and roll angle
798 rescueState.intent.targetVelocityCmS = gpsRescueConfig()->rescueGroundspeed * angleToHome;
799 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->angle * angleToHome;
800 rescueState.intent.rollAngleLimitDeg = gpsRescueConfig()->angle * angleToHome;
801 if (rescueState.sensor.absErrorAngle < 10.0f) {
802 // enter fly home phase with full forward velocity target and full angle values
803 rescueState.phase = RESCUE_FLY_HOME;
804 rescueState.intent.secondsFailing = 0; // reset sanity timer for flight home
805 rescueState.intent.yawAttenuator = 1.0f;
806 rescueState.intent.targetVelocityCmS = gpsRescueConfig()->rescueGroundspeed;
807 rescueState.intent.pitchAngleLimitDeg = gpsRescueConfig()->angle;
808 rescueState.intent.rollAngleLimitDeg = gpsRescueConfig()->angle;
811 break;
813 case RESCUE_FLY_HOME:
814 // fly home with full control on all axes, pitching forward to gain speed
815 if (newGPSData) {
816 if (rescueState.sensor.distanceToHomeM <= rescueState.intent.descentDistanceM) {
817 rescueState.phase = RESCUE_DESCENT;
818 rescueState.intent.secondsFailing = 0; // reset sanity timer for descent
821 break;
823 case RESCUE_DESCENT:
824 // attenuate velocity and altitude targets while updating the heading to home
825 // once inside the landing box, stop rotating, just descend
826 if (rescueState.sensor.currentAltitudeCm < rescueState.intent.targetLandingAltitudeCm) {
827 // enter landing mode once below landing altitude
828 rescueState.phase = RESCUE_LANDING;
829 rescueState.intent.secondsFailing = 0; // reset sanity timer for landing
830 rescueState.intent.targetVelocityCmS = 0; // zero velocity to home
831 rescueState.intent.pitchAngleLimitDeg = halfAngle; // reduced pitch angles
832 rescueState.intent.rollAngleLimitDeg = 0; // no roll while landing
834 if (newGPSData) {
835 const float distanceToLandingAreaM = rescueState.sensor.distanceToHomeM - 2.0f;
836 // considers home to be a 2m circle around home to avoid overshooting home point
837 proximityToLandingArea = constrainf(distanceToLandingAreaM / rescueState.intent.descentDistanceM, 0.0f, 1.0f);
838 rescueState.intent.targetVelocityCmS = gpsRescueConfig()->rescueGroundspeed * proximityToLandingArea;
839 // reduce target velocity as we get closer to home. Zero within 2m of home, reducing risk of overshooting.
840 // if quad drifts further than 2m away from home, should by then have rotated towards home, and pitch is allowed
841 rescueState.intent.rollAngleLimitDeg = gpsRescueConfig()->angle * proximityToLandingArea;
842 // reduce roll capability when closer to home, none within final 2m
844 descend();
845 break;
847 case RESCUE_LANDING:
848 // control yaw angle, throttle and pitch. no roll. descend steadily until impact, then disarm.
849 descend();
850 disarmOnImpact();
851 break;
853 case RESCUE_COMPLETE:
854 rescueStop();
855 break;
857 case RESCUE_ABORT:
858 setArmingDisabled(ARMING_DISABLED_ARM_SWITCH);
859 disarm(DISARM_REASON_GPS_RESCUE);
860 rescueStop();
861 break;
863 case RESCUE_DO_NOTHING:
864 disarmOnImpact();
865 break;
867 default:
868 break;
871 DEBUG_SET(DEBUG_GPS_RESCUE_TRACKING, 3, rescueState.intent.targetAltitudeCm);
872 DEBUG_SET(DEBUG_GPS_RESCUE_THROTTLE_PID, 3, rescueState.intent.targetAltitudeCm);
873 DEBUG_SET(DEBUG_RTH, 1, rescueState.phase);
875 performSanityChecks();
876 rescueAttainPosition();
878 newGPSData = false;
881 float gpsRescueGetYawRate(void)
883 return rescueYaw;
886 float gpsRescueGetThrottle(void)
888 // Calculated a desired commanded throttle scaled from 0.0 to 1.0 for use in the mixer.
889 // We need to compensate for min_check since the throttle value set by gps rescue
890 // is based on the raw rcCommand value commanded by the pilot.
891 float commandedThrottle = scaleRangef(rescueThrottle, MAX(rxConfig()->mincheck, PWM_RANGE_MIN), PWM_RANGE_MAX, 0.0f, 1.0f);
892 commandedThrottle = constrainf(commandedThrottle, 0.0f, 1.0f);
894 return commandedThrottle;
897 bool gpsRescueIsConfigured(void)
899 return failsafeConfig()->failsafe_procedure == FAILSAFE_PROCEDURE_GPS_RESCUE || isModeActivationConditionPresent(BOXGPSRESCUE);
902 bool gpsRescueIsAvailable(void)
904 return rescueState.isAvailable;
907 bool gpsRescueIsDisabled(void)
908 // used for OSD warning
910 return (!STATE(GPS_FIX_HOME));
913 #ifdef USE_MAG
914 bool gpsRescueDisableMag(void)
916 return ((!gpsRescueConfig()->useMag || magForceDisable) && (rescueState.phase >= RESCUE_INITIALIZE) && (rescueState.phase <= RESCUE_LANDING));
918 #endif
919 #endif