P7a of the GO 4-parity plan. The rail's GRID button becomes MODES, which opens a picker of the product's intelligent flight modes with first-use terms; Waypoints is the first one built. Waypoints opens on the big map: tap to add waypoints or POIs, long-press to drag, "+" to insert on a leg, and a point editor for altitude, speed, heading (route / custom / POI), gimbal (set or aim at POI), camera action and hover, and turn radius, with a swipe-to-adjust readout. Route settings, a local mission library and undo sit in a tool column. Start runs the SafetyGate's pre-start check in the slide-to-confirm dialog, then uploads and starts; a run panel shows upload and flight progress with Pause, Resume and Stop. MissionBridge now listens to the waypoint operator and builds the full V1 mission (per-waypoint headings, turn direction, gimbal pitch, actions). Not yet run on a simulator or aircraft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
479 lines
22 KiB
Dart
479 lines
22 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:latlong2/latlong.dart' show LatLng;
|
||
|
||
import '../dji_service.dart';
|
||
import '../flight_model.dart';
|
||
import 'action_rail_state.dart';
|
||
import 'calibration.dart';
|
||
import 'flight_status.dart';
|
||
import 'hud_geometry.dart';
|
||
import 'modes/waypoint_mission.dart';
|
||
import 'state/aircraft_state.dart';
|
||
import 'state/alert_state.dart';
|
||
import 'state/mission_state.dart';
|
||
|
||
export 'action_rail_state.dart' show FlightCommand;
|
||
|
||
/// How hard a precondition stops a command.
|
||
enum CheckLevel {
|
||
/// Shown on the confirm dialog; the pilot may still slide.
|
||
warn,
|
||
|
||
/// The command is refused; the dialog says why and has no slider.
|
||
block,
|
||
}
|
||
|
||
@immutable
|
||
class GateCheck {
|
||
const GateCheck(this.level, this.message);
|
||
|
||
const GateCheck.warn(this.message) : level = CheckLevel.warn;
|
||
const GateCheck.block(this.message) : level = CheckLevel.block;
|
||
|
||
final CheckLevel level;
|
||
final String message;
|
||
|
||
bool get blocks => level == CheckLevel.block;
|
||
|
||
@override
|
||
bool operator ==(Object other) => other is GateCheck && other.level == level && other.message == message;
|
||
|
||
@override
|
||
int get hashCode => Object.hash(level, message);
|
||
|
||
@override
|
||
String toString() => '${level.name}: $message';
|
||
}
|
||
|
||
/// What [SafetyGate.check] found for one command.
|
||
@immutable
|
||
class GateVerdict {
|
||
const GateVerdict(this.command, this.checks);
|
||
|
||
final FlightCommand command;
|
||
final List<GateCheck> checks;
|
||
|
||
bool get allowed => !checks.any((GateCheck c) => c.blocks);
|
||
List<GateCheck> get blockers => <GateCheck>[for (final GateCheck c in checks) if (c.blocks) c];
|
||
List<GateCheck> get warnings => <GateCheck>[for (final GateCheck c in checks) if (!c.blocks) c];
|
||
}
|
||
|
||
/// Thrown by [SafetyGate.run] when a command's preconditions fail at the
|
||
/// moment it would be sent (the state may have moved on since the dialog).
|
||
class SafetyGateRefused implements Exception {
|
||
const SafetyGateRefused(this.verdict);
|
||
|
||
final GateVerdict verdict;
|
||
|
||
@override
|
||
String toString() => verdict.blockers.map((GateCheck c) => c.message).join(' ');
|
||
}
|
||
|
||
/// Thrown by [SafetyGate.runMaintenance] when a calibration may not start.
|
||
class MaintenanceRefused implements Exception {
|
||
const MaintenanceRefused(this.checks);
|
||
|
||
final List<GateCheck> checks;
|
||
|
||
@override
|
||
String toString() => checks.where((GateCheck c) => c.blocks).map((GateCheck c) => c.message).join(' ');
|
||
}
|
||
|
||
/// Thrown by the mission methods of [SafetyGate] when a mission may not start or go on.
|
||
class MissionRefused implements Exception {
|
||
const MissionRefused(this.checks);
|
||
|
||
final List<GateCheck> checks;
|
||
|
||
@override
|
||
String toString() => checks.where((GateCheck c) => c.blocks).map((GateCheck c) => c.message).join(' ');
|
||
}
|
||
|
||
/// The one place every aircraft-moving command passes through (plan §1.5, P3).
|
||
///
|
||
/// [check] grades the preconditions — connected, flight state, GPS, battery,
|
||
/// live faults — into blockers and warnings for the slide-to-confirm dialog;
|
||
/// [run] re-checks and then sends the command. The UI never calls the
|
||
/// `DjiService` flight commands directly, so this class is what the safety
|
||
/// tests pin down.
|
||
class SafetyGate {
|
||
SafetyGate({required this.model, required this.dji});
|
||
|
||
final FlightModel model;
|
||
final DjiService dji;
|
||
|
||
/// RTH altitude limits the flight controller accepts (MSDK `setGoHomeHeightInMeters`).
|
||
static const int minRthHeight = 20;
|
||
static const int maxRthHeight = 500;
|
||
|
||
/// Below this RTH altitude the dialog warns about obstacles on the way home.
|
||
static const int lowRthHeight = 30;
|
||
|
||
/// How far the pilot must be from the home point before RTH asks where to go.
|
||
static const double rthTargetChoiceDistance = 30;
|
||
|
||
AircraftState get _a => model.aircraft;
|
||
|
||
GateVerdict check(FlightCommand command) {
|
||
final List<GateCheck> checks = <GateCheck>[];
|
||
if (!model.connected) {
|
||
checks.add(const GateCheck.block('No aircraft connected.'));
|
||
return GateVerdict(command, checks);
|
||
}
|
||
if (_a.updatedAt == null) {
|
||
checks.add(const GateCheck.block('Waiting for the flight controller to report.'));
|
||
return GateVerdict(command, checks);
|
||
}
|
||
switch (command) {
|
||
case FlightCommand.takeOff:
|
||
_takeOff(checks);
|
||
case FlightCommand.land:
|
||
if (_a.isFlying != true) checks.add(const GateCheck.block('The aircraft is not flying.'));
|
||
if (_a.goingHome == true) checks.add(const GateCheck.warn('This ends Return to Home and lands where the aircraft is now.'));
|
||
case FlightCommand.cancelLanding:
|
||
if (_a.flightModeEnum != 'AUTO_LANDING') checks.add(const GateCheck.block('The aircraft is not landing.'));
|
||
case FlightCommand.confirmLanding:
|
||
if (_a.landingConfirmationNeeded != true && _a.flightModeEnum != 'CONFIRM_LANDING') {
|
||
checks.add(const GateCheck.block('The aircraft is not waiting for a landing confirmation.'));
|
||
}
|
||
case FlightCommand.goHome:
|
||
_goHome(checks);
|
||
case FlightCommand.cancelGoHome:
|
||
if (_a.goingHome != true && _a.flightModeEnum != 'GO_HOME') {
|
||
checks.add(const GateCheck.block('The aircraft is not returning to home.'));
|
||
}
|
||
case FlightCommand.stopMode:
|
||
switch (RailState.activeMode(_a, model.mission)) {
|
||
case ActiveMode.none:
|
||
checks.add(const GateCheck.block('No intelligent flight mode is running.'));
|
||
case ActiveMode.other:
|
||
checks.add(const GateCheck.block('This mode cannot be stopped from the app yet. Press the pause button on the remote.'));
|
||
case ActiveMode.waypoint:
|
||
case ActiveMode.activeTrack:
|
||
break;
|
||
}
|
||
}
|
||
return GateVerdict(command, checks);
|
||
}
|
||
|
||
void _takeOff(List<GateCheck> checks) {
|
||
if (_a.isFlying == true) {
|
||
checks.add(const GateCheck.block('The aircraft is already flying.'));
|
||
return;
|
||
}
|
||
final int? percent = model.battery.percent;
|
||
final int critical = model.battery.seriousLowWarningThreshold ?? 15;
|
||
final int low = model.battery.lowWarningThreshold ?? 30;
|
||
if (_a.seriousLowBattery == true || (percent != null && percent <= critical)) {
|
||
checks.add(GateCheck.block('Battery critically low${percent == null ? '' : ' ($percent%)'}. Replace or charge it.'));
|
||
} else if (_a.lowBattery == true || (percent != null && percent <= low)) {
|
||
checks.add(GateCheck.warn('Battery low${percent == null ? '' : ' ($percent%)'}: a short flight only.'));
|
||
}
|
||
if (_a.imuPreheating == true) checks.add(const GateCheck.block('IMU is warming up. Wait until it is ready.'));
|
||
if (_a.compassCalibrating == true) checks.add(const GateCheck.block('Compass calibration is in progress.'));
|
||
if (_a.compassHasError == true) checks.add(const GateCheck.block('Compass error. Calibrate the compass or move away from metal.'));
|
||
// Faults the aircraft itself reports: critical ones refuse, the worst warning is shown.
|
||
final Iterable<DiagnosticAlert> faults = model.alerts.all.where((DiagnosticAlert d) => d.severity == AlertSeverity.critical);
|
||
for (final DiagnosticAlert d in faults.take(2)) {
|
||
checks.add(GateCheck.block(d.reason));
|
||
}
|
||
final bool gps = (_a.gpsSignalLevel ?? 0) >= FlightStatus.minGpsLevel;
|
||
if (!gps) {
|
||
checks.add(GateCheck.warn(_a.visionPositioningBeingUsed == true
|
||
? 'Weak GPS: the aircraft holds position by vision only, low over textured ground.'
|
||
: 'Weak GPS: the aircraft will fly in ATTI mode and drift with the wind.'));
|
||
}
|
||
if (_a.homeLocationSet != true) {
|
||
checks.add(const GateCheck.warn('No home point recorded yet: Return to Home will not be available.'));
|
||
}
|
||
for (final DiagnosticAlert d in model.alerts.all.where((DiagnosticAlert d) => d.severity == AlertSeverity.warning).take(1)) {
|
||
checks.add(GateCheck.warn(d.reason));
|
||
}
|
||
if (model.profile.hasCamera && model.camera.storage != null && !model.camera.storage!.usable) {
|
||
checks.add(GateCheck.warn('${model.camera.storage!.status}: photos and video will not be saved.'));
|
||
}
|
||
}
|
||
|
||
void _goHome(List<GateCheck> checks) {
|
||
if (_a.isFlying != true) {
|
||
checks.add(const GateCheck.block('The aircraft is not flying.'));
|
||
return;
|
||
}
|
||
if (_a.goingHome == true) {
|
||
checks.add(const GateCheck.block('The aircraft is already returning to home.'));
|
||
return;
|
||
}
|
||
if (_a.homeLocationSet != true) {
|
||
checks.add(const GateCheck.block('No home point recorded. Fly back manually.'));
|
||
}
|
||
if ((_a.gpsSignalLevel ?? 0) < FlightStatus.minGpsLevel) {
|
||
checks.add(const GateCheck.block('GPS too weak for Return to Home. Fly back manually.'));
|
||
}
|
||
final int? percent = model.battery.percent;
|
||
final int? needed = _a.goHome?.batteryNeededToGoHome;
|
||
if (percent != null && needed != null && needed > 0 && percent < needed) {
|
||
checks.add(GateCheck.warn('Battery ($percent%) is below what the trip home needs ($needed%). It may land on the way.'));
|
||
}
|
||
final double? h = _a.goHomeHeight;
|
||
if (h != null && h < lowRthHeight && !model.profile.hasObstacleSensing) {
|
||
checks.add(GateCheck.warn('RTH altitude is ${h.round()} m and this aircraft cannot see obstacles.'));
|
||
}
|
||
}
|
||
|
||
// ── Calibration & maintenance (plan P6b) ────────────────────────────────────
|
||
|
||
/// Below this charge an IMU calibration (5–10 minutes) is discouraged.
|
||
static const int imuMinBattery = 30;
|
||
|
||
/// Preconditions for starting [kind]. Everything except the center-of-gravity
|
||
/// calibration (done in a hover) and the gimbal trim needs the aircraft on
|
||
/// the ground with the motors off.
|
||
List<GateCheck> checkMaintenance(CalibrationKind kind) {
|
||
final List<GateCheck> checks = <GateCheck>[];
|
||
if (kind == CalibrationKind.visionSensors) return checks;
|
||
if (!model.connected) return checks..add(const GateCheck.block('No aircraft connected.'));
|
||
if (_a.updatedAt == null) return checks..add(const GateCheck.block('Waiting for the flight controller to report.'));
|
||
switch (kind) {
|
||
case CalibrationKind.gimbalAdjust:
|
||
case CalibrationKind.visionSensors:
|
||
break;
|
||
case CalibrationKind.gravityCenter:
|
||
if (_a.isFlying != true) checks.add(const GateCheck.block('Take off and hover first: the aircraft measures its balance in the air.'));
|
||
if ((_a.gpsSignalLevel ?? 0) < FlightStatus.minGpsLevel) checks.add(const GateCheck.block('GPS too weak to hold a steady hover.'));
|
||
if (_a.windWarning != null && _a.windWarning != 'LEVEL_0') checks.add(const GateCheck.warn('Strong wind: the result may be off.'));
|
||
default:
|
||
if (_a.isFlying == true) {
|
||
checks.add(const GateCheck.block('Land first.'));
|
||
} else if (_a.motorsOn == true) {
|
||
checks.add(const GateCheck.block('Stop the motors first.'));
|
||
}
|
||
}
|
||
switch (kind) {
|
||
case CalibrationKind.imu:
|
||
final int? percent = model.battery.percent;
|
||
if (percent != null && percent < imuMinBattery) checks.add(GateCheck.warn('Battery at $percent%: calibration takes up to 10 minutes.'));
|
||
if (_a.imuPreheating == true) checks.add(const GateCheck.warn('The IMU is warming up; calibration waits for it.'));
|
||
case CalibrationKind.compass:
|
||
if (_a.compassCalibrating == true) checks.add(const GateCheck.warn('A compass calibration is already running; starting restarts it.'));
|
||
case CalibrationKind.gimbal:
|
||
if (model.gimbal.calibrating) checks.add(const GateCheck.block('The gimbal is already calibrating.'));
|
||
case CalibrationKind.propeller:
|
||
checks.add(const GateCheck.warn('The motors spin during calibration. Keep clear of the propellers.'));
|
||
case CalibrationKind.sensorCleaning:
|
||
case CalibrationKind.lens:
|
||
if (model.camera.isRecording) checks.add(const GateCheck.block('Stop recording first.'));
|
||
default:
|
||
break;
|
||
}
|
||
return checks;
|
||
}
|
||
|
||
/// Re-checks [kind] and runs [start] — the wizard's start command.
|
||
Future<void> runMaintenance(CalibrationKind kind, Future<void> Function() start) async {
|
||
final List<GateCheck> checks = checkMaintenance(kind);
|
||
if (checks.any((GateCheck c) => c.blocks)) throw MaintenanceRefused(checks);
|
||
await start();
|
||
}
|
||
|
||
// ── Intelligent flight modes (plan P7) ──────────────────────────────────────
|
||
|
||
/// Beyond this the pre-start check points out the trip to waypoint 1.
|
||
static const double farFirstWaypoint = 500;
|
||
|
||
/// The aircraft's own limits for [WaypointMissionSpec.validate], read from
|
||
/// the flight controller (they are not in the telemetry tick).
|
||
Future<MissionLimits> missionLimits() async {
|
||
Future<Object?> read(String key) async {
|
||
if (!model.connected) return null;
|
||
try {
|
||
return await dji.getKey(DjiKey.flightController(key));
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
final List<Object?> v = await Future.wait(<Future<Object?>>[
|
||
read('MAX_FLIGHT_HEIGHT'),
|
||
read('MAX_FLIGHT_RADIUS'),
|
||
read('MAX_FLIGHT_RADIUS_ENABLED'),
|
||
]);
|
||
final double? lat = _a.homeLatitude, lon = _a.homeLongitude;
|
||
final bool radiusOn = v[2] == true;
|
||
return MissionLimits(
|
||
maxAltitude: (v[0] as num?)?.toDouble(),
|
||
maxRadius: radiusOn ? (v[1] as num?)?.toDouble() : null,
|
||
home: lat == null || lon == null || _a.homeLocationSet == false ? null : LatLng(lat, lon),
|
||
gimbalMin: model.profile.gimbal?.pitch?.min ?? -90,
|
||
gimbalMax: model.profile.gimbal?.pitch?.max ?? 30,
|
||
);
|
||
}
|
||
|
||
/// Preconditions for flying [spec]: the aircraft's state, the operator's,
|
||
/// and the route's own [WaypointMissionSpec.validate] issues.
|
||
List<GateCheck> checkWaypointStart(WaypointMissionSpec spec, [MissionLimits limits = const MissionLimits()]) {
|
||
final List<GateCheck> checks = <GateCheck>[];
|
||
if (!model.connected) return checks..add(const GateCheck.block('No aircraft connected.'));
|
||
if (_a.updatedAt == null) return checks..add(const GateCheck.block('Waiting for the flight controller to report.'));
|
||
|
||
final WaypointProgress op = model.mission.waypoint;
|
||
if (op.unsupported) {
|
||
checks.add(const GateCheck.block('This aircraft cannot fly waypoint missions.'));
|
||
} else if (op.active) {
|
||
checks.add(const GateCheck.block('A waypoint mission is already running.'));
|
||
} else if (op.uploading) {
|
||
checks.add(const GateCheck.block('A mission is still uploading.'));
|
||
} else if (op.state == 'DISCONNECTED' || op.state == 'RECOVERING') {
|
||
checks.add(const GateCheck.block('The mission system is not ready yet. Wait a moment.'));
|
||
}
|
||
if (model.mission.tracking) checks.add(const GateCheck.block('Stop ActiveTrack first.'));
|
||
|
||
for (final MissionIssue i in spec.validate(limits)) {
|
||
checks.add(i.blocks ? GateCheck.block(i.message) : GateCheck.warn(i.message));
|
||
}
|
||
|
||
final String mode = _a.flightModeEnum ?? '';
|
||
if (_a.goingHome == true || mode == 'GO_HOME') checks.add(const GateCheck.block('Cancel Return to Home first.'));
|
||
if (mode == 'AUTO_LANDING' || mode == 'CONFIRM_LANDING') checks.add(const GateCheck.block('The aircraft is landing.'));
|
||
if ((_a.gpsSignalLevel ?? 0) < FlightStatus.minGpsLevel) {
|
||
checks.add(const GateCheck.block('GPS too weak for a waypoint mission.'));
|
||
}
|
||
if (_a.homeLocationSet != true) checks.add(const GateCheck.block('No home point recorded yet.'));
|
||
|
||
final int? percent = model.battery.percent;
|
||
final int critical = model.battery.seriousLowWarningThreshold ?? 15;
|
||
final int low = model.battery.lowWarningThreshold ?? 30;
|
||
if (_a.seriousLowBattery == true || (percent != null && percent <= critical)) {
|
||
checks.add(GateCheck.block('Battery critically low${percent == null ? '' : ' ($percent%)'}.'));
|
||
} else if (_a.lowBattery == true || (percent != null && percent <= low)) {
|
||
checks.add(GateCheck.warn('Battery low${percent == null ? '' : ' ($percent%)'}: the route may not finish.'));
|
||
}
|
||
final int? left = _a.goHome?.remainingFlightTime;
|
||
final int needed = spec.estimatedSeconds + (_a.goHome?.timeNeededToGoHome ?? 0);
|
||
if (left != null && left > 0 && spec.points.length >= 2 && needed > left) {
|
||
checks.add(GateCheck.warn('The route takes about ${formatDuration(spec.estimatedSeconds)}; '
|
||
'the battery has about ${formatDuration(left)} of flight left.'));
|
||
}
|
||
|
||
if (_a.isFlying != true) {
|
||
if (_a.imuPreheating == true) checks.add(const GateCheck.block('IMU is warming up. Wait until it is ready.'));
|
||
if (_a.compassCalibrating == true) checks.add(const GateCheck.block('Compass calibration is in progress.'));
|
||
for (final DiagnosticAlert d in model.alerts.all.where((DiagnosticAlert d) => d.severity == AlertSeverity.critical).take(2)) {
|
||
checks.add(GateCheck.block(d.reason));
|
||
}
|
||
checks.add(const GateCheck.warn('The aircraft takes off and flies to waypoint 1 on its own.'));
|
||
}
|
||
if (_a.compassHasError == true) checks.add(const GateCheck.block('Compass error. Calibrate the compass or move away from metal.'));
|
||
|
||
final double? lat = _a.latitude, lon = _a.longitude;
|
||
if (spec.points.isNotEmpty && lat != null && lon != null && !(lat == 0 && lon == 0)) {
|
||
final LatLng first = spec.points.first.at;
|
||
final double d = HudGeometry.distance(lat, lon, first.latitude, first.longitude);
|
||
if (d > farFirstWaypoint) checks.add(GateCheck.warn('Waypoint 1 is ${(d / 1000).toStringAsFixed(1)} km from the aircraft.'));
|
||
}
|
||
if (!model.profile.hasObstacleSensing && spec.points.any((WaypointSpec p) => p.altitude < lowRthHeight)) {
|
||
checks.add(const GateCheck.warn('Parts of the route are low and this aircraft cannot see obstacles.'));
|
||
}
|
||
final bool shoots = spec.points.any((WaypointSpec p) => p.action != WaypointCameraAction.none);
|
||
if (shoots && model.camera.storage != null && !model.camera.storage!.usable) {
|
||
checks.add(GateCheck.warn("${model.camera.storage!.status}: the route's photos and video will not be saved."));
|
||
}
|
||
return checks;
|
||
}
|
||
|
||
/// Re-checks [spec], writes [rthHeight] when it changed, uploads the
|
||
/// mission and starts it. [onUploaded] runs between the two.
|
||
Future<void> startWaypointMission(
|
||
WaypointMissionSpec spec, {
|
||
MissionLimits limits = const MissionLimits(),
|
||
int? rthHeight,
|
||
void Function()? onUploaded,
|
||
}) async {
|
||
final List<GateCheck> checks = checkWaypointStart(spec, limits);
|
||
if (checks.any((GateCheck c) => c.blocks)) throw MissionRefused(checks);
|
||
if (rthHeight != null) {
|
||
final int h = rthHeight.clamp(minRthHeight, maxRthHeight);
|
||
if (h != _a.goHomeHeight?.round()) await dji.setGoHomeHeight(h);
|
||
}
|
||
await dji.uploadWaypointMission(spec.toBridge(gimbalMin: limits.gimbalMin, gimbalMax: limits.gimbalMax));
|
||
onUploaded?.call();
|
||
await dji.startWaypointMission();
|
||
}
|
||
|
||
/// Holds the aircraft where it is; the mission can resume from there.
|
||
Future<void> pauseWaypointMission() async {
|
||
if (!model.mission.waypoint.executing) {
|
||
throw const MissionRefused(<GateCheck>[GateCheck.block('No waypoint mission is flying.')]);
|
||
}
|
||
await dji.pauseWaypointMission();
|
||
}
|
||
|
||
Future<void> resumeWaypointMission() async {
|
||
final List<GateCheck> checks = <GateCheck>[
|
||
if (!model.mission.waypoint.paused) const GateCheck.block('The mission is not paused.'),
|
||
if ((_a.gpsSignalLevel ?? 0) < FlightStatus.minGpsLevel) const GateCheck.block('GPS too weak to carry on with the route.'),
|
||
if (_a.goingHome == true) const GateCheck.block('The aircraft is returning to home.'),
|
||
];
|
||
if (checks.isNotEmpty) throw MissionRefused(checks);
|
||
await dji.resumeWaypointMission();
|
||
}
|
||
|
||
/// True when the pilot stands far enough from the recorded home point that
|
||
/// RTH should ask whether to come back to them instead (GO 4's RTH target).
|
||
bool get offersRthTarget => (pilotToHome ?? 0) > rthTargetChoiceDistance;
|
||
|
||
/// Metres between the pilot (the phone's fix) and the recorded home point.
|
||
double? get pilotToHome {
|
||
final double? pLat = model.phoneLatitude, pLon = model.phoneLongitude;
|
||
final double? hLat = _a.homeLatitude, hLon = _a.homeLongitude;
|
||
if (pLat == null || pLon == null || hLat == null || hLon == null) return null;
|
||
return HudGeometry.distance(pLat, pLon, hLat, hLon);
|
||
}
|
||
|
||
/// Answers the flight controller's low-battery smart-RTH countdown: true
|
||
/// flies home now, false cancels it. Ignored once the countdown has ended.
|
||
Future<void> answerSmartRth({required bool goHome}) async {
|
||
if (_a.goHome?.smartRTHCountingDown != true) return;
|
||
await dji.confirmSmartRTH(goHome);
|
||
}
|
||
|
||
/// Re-checks [command] and sends it. For take-off, [precision] asks for a
|
||
/// precision take-off; for RTH, [rthHeight] is written first when it differs
|
||
/// from the current setting and [homeToPilot] moves the home point to the
|
||
/// phone's position.
|
||
Future<void> run(FlightCommand command, {bool precision = false, int? rthHeight, bool homeToPilot = false}) async {
|
||
final GateVerdict v = check(command);
|
||
if (!v.allowed) throw SafetyGateRefused(v);
|
||
switch (command) {
|
||
case FlightCommand.takeOff:
|
||
await (precision ? dji.precisionTakeOff() : dji.takeOff());
|
||
case FlightCommand.land:
|
||
await dji.land();
|
||
case FlightCommand.cancelLanding:
|
||
await dji.cancelLanding();
|
||
case FlightCommand.confirmLanding:
|
||
await dji.confirmLanding();
|
||
case FlightCommand.goHome:
|
||
if (homeToPilot && model.phoneLatitude != null && model.phoneLongitude != null) {
|
||
await dji.setHomeLocation(model.phoneLatitude!, model.phoneLongitude!);
|
||
}
|
||
if (rthHeight != null) {
|
||
final int h = rthHeight.clamp(minRthHeight, maxRthHeight);
|
||
if (h != _a.goHomeHeight?.round()) await dji.setGoHomeHeight(h);
|
||
}
|
||
await dji.startGoHome();
|
||
case FlightCommand.cancelGoHome:
|
||
await dji.cancelGoHome();
|
||
case FlightCommand.stopMode:
|
||
switch (RailState.activeMode(_a, model.mission)) {
|
||
case ActiveMode.waypoint:
|
||
await dji.stopWaypointMission();
|
||
case ActiveMode.activeTrack:
|
||
await dji.stopActiveTrack();
|
||
case ActiveMode.none:
|
||
case ActiveMode.other:
|
||
break; // refused by check()
|
||
}
|
||
}
|
||
}
|
||
}
|