Take-off, land, RTH, the landing cancel/confirm and Stop now all go through one SafetyGate (lib/fly/safety_gate.dart). The gate sorts its preconditions into blockers and warnings: connection, flight state, battery against the thresholds, IMU warm-up, compass, critical diagnostics, GPS, home point, SD card and the RTH battery budget. It checks again right before sending. The left rail is now a state machine (lib/fly/action_rail_state.dart): Take-off becomes Land, then Cancel or Confirm landing. RTH becomes Cancel RTH. A Stop button shows while an intelligent mode is flying. Moving the aircraft needs a slide-to-confirm dialog, which can offer Precision Takeoff or set the RTH altitude. A blocked command shows why and has no slider. When the pilot is more than 30 m from home, RTH asks whether to return to them or to the original home point. The HUD gains an error stack. Its cards can be dismissed, it fits the space it has, and a new warning triggers haptic feedback. It also gets a smart-RTH countdown banner with Cancel / Go home now, a forced-landing banner, and an Aircraft Status sheet. The sheet opens from the status chip and appears before each connection's first take-off. It offers Calibrate compass/IMU and Format SD. On the bridge side this adds precisionTakeOff, setHomeLocation, compass and IMU calibration start/stop, formatStorage, and compass health in the telemetry tick. The classic flight screen stays available until the simulator run passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
6.1 KiB
Dart
150 lines
6.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../fly/hud_features.dart';
|
|
import '../../fly/product_profile.dart';
|
|
import '../../fly/safety_gate.dart';
|
|
import '../../theme.dart';
|
|
import 'dialogs/rth_target_dialog.dart';
|
|
import 'dialogs/slide_to_confirm.dart';
|
|
import 'hud_kit.dart';
|
|
|
|
/// Turns a rail press into the GO 4 confirm flow (plan P3): ask the
|
|
/// [SafetyGate], offer the Aircraft Status sheet before the first take-off of a
|
|
/// connection, ask the RTH target when the pilot has moved, show the
|
|
/// slide-to-confirm dialog, then let the gate send the command.
|
|
///
|
|
/// Commands that stop the aircraft moving (cancel landing, cancel RTH, stop an
|
|
/// intelligent mode) skip the slider: they make it hover, and a pilot reaching
|
|
/// for them is in a hurry. They still go through the gate.
|
|
class FlightCommandFlow {
|
|
FlightCommandFlow({required this.gate, required this.onError, this.reviewBeforeTakeOff});
|
|
|
|
final SafetyGate gate;
|
|
final void Function(String label, Object error) onError;
|
|
|
|
/// Opens the Aircraft Status sheet offering take-off; resolves true when the
|
|
/// pilot chose to continue.
|
|
final Future<bool> Function(BuildContext context)? reviewBeforeTakeOff;
|
|
|
|
bool _reviewed = false;
|
|
|
|
/// The status sheet is offered again before the next connection's first take-off.
|
|
void resetReview() => _reviewed = false;
|
|
|
|
/// Take-off's own review step is done (the sheet was opened from the status chip).
|
|
void markReviewed() => _reviewed = true;
|
|
|
|
static String labelOf(FlightCommand c) => switch (c) {
|
|
FlightCommand.takeOff => 'Take off',
|
|
FlightCommand.land => 'Land',
|
|
FlightCommand.cancelLanding => 'Cancel landing',
|
|
FlightCommand.confirmLanding => 'Confirm landing',
|
|
FlightCommand.goHome => 'Return to Home',
|
|
FlightCommand.cancelGoHome => 'Cancel RTH',
|
|
FlightCommand.stopMode => 'Stop',
|
|
};
|
|
|
|
static bool _immediate(FlightCommand c) =>
|
|
c == FlightCommand.cancelLanding || c == FlightCommand.cancelGoHome || c == FlightCommand.stopMode;
|
|
|
|
Future<void> request(BuildContext context, FlightCommand command) async {
|
|
GateVerdict verdict = gate.check(command);
|
|
|
|
if (_immediate(command) && verdict.allowed) {
|
|
await _send(command);
|
|
return;
|
|
}
|
|
|
|
if (command == FlightCommand.takeOff && verdict.allowed && !_reviewed && reviewBeforeTakeOff != null) {
|
|
_reviewed = true;
|
|
final bool go = await reviewBeforeTakeOff!(context);
|
|
if (!go || !context.mounted) return;
|
|
verdict = gate.check(command); // the sheet may have been open a while
|
|
}
|
|
|
|
RthTarget target = RthTarget.originalHome;
|
|
if (command == FlightCommand.goHome && verdict.allowed && gate.offersRthTarget) {
|
|
final RthTarget? t = await showRthTargetDialog(context, pilotToHomeM: gate.pilotToHome!);
|
|
if (t == null || !context.mounted) return;
|
|
target = t;
|
|
}
|
|
|
|
if (!context.mounted) return;
|
|
final SlideResult? r = await showSlideToConfirm(context, _spec(context, command, verdict, target));
|
|
if (r == null) return;
|
|
await _send(
|
|
command,
|
|
precision: r.checked,
|
|
rthHeight: command == FlightCommand.goHome ? r.number : null,
|
|
homeToPilot: target == RthTarget.pilot,
|
|
);
|
|
}
|
|
|
|
Future<void> _send(FlightCommand command, {bool precision = false, int? rthHeight, bool homeToPilot = false}) async {
|
|
try {
|
|
await gate.run(command, precision: precision, rthHeight: rthHeight, homeToPilot: homeToPilot);
|
|
} catch (e) {
|
|
onError(labelOf(command), e);
|
|
}
|
|
}
|
|
|
|
SlideConfirmSpec _spec(BuildContext context, FlightCommand c, GateVerdict v, RthTarget target) {
|
|
switch (c) {
|
|
case FlightCommand.takeOff:
|
|
final ProductProfile p = ProductProfileScope.of(context);
|
|
final bool precision = p.connected && HudFeatures.precisionTakeoff.enabledFor(p);
|
|
return SlideConfirmSpec(
|
|
icon: 'takeoff',
|
|
title: 'Take off',
|
|
description: 'The aircraft will take off and hover at about 1.2 m. Keep people clear of the propellers.',
|
|
sliderLabel: 'Slide to take off',
|
|
checks: v.checks,
|
|
checkbox: precision ? 'Precision Takeoff' : null,
|
|
checkboxHint: precision ? 'Records the take-off spot with the downward cameras, so RTH lands back on it.' : null,
|
|
);
|
|
case FlightCommand.land:
|
|
return SlideConfirmSpec(
|
|
icon: 'land',
|
|
title: 'Land',
|
|
description: 'The aircraft will descend and land where it is now. Make sure the ground below is clear and level.',
|
|
sliderLabel: 'Slide to land',
|
|
color: HudColors.caution,
|
|
checks: v.checks,
|
|
);
|
|
case FlightCommand.confirmLanding:
|
|
return SlideConfirmSpec(
|
|
icon: 'land',
|
|
title: 'Confirm landing',
|
|
description: 'The aircraft is hovering just above the ground and needs your OK to touch down. Check the spot is flat and clear.',
|
|
sliderLabel: 'Slide to land',
|
|
color: HudColors.caution,
|
|
checks: v.checks,
|
|
);
|
|
case FlightCommand.goHome:
|
|
final int current = (gate.model.aircraft.goHomeHeight ?? 60).round().clamp(SafetyGate.minRthHeight, SafetyGate.maxRthHeight);
|
|
return SlideConfirmSpec(
|
|
icon: 'rth',
|
|
title: 'Return to Home',
|
|
description: target == RthTarget.pilot
|
|
? 'The home point moves to your position. The aircraft climbs to the RTH altitude, flies back to you and lands.'
|
|
: 'The aircraft climbs to the RTH altitude, flies back to the home point and lands.',
|
|
sliderLabel: 'Slide to return home',
|
|
checks: v.checks,
|
|
number: SlideNumber(label: 'RTH altitude', initial: current, min: SafetyGate.minRthHeight, max: SafetyGate.maxRthHeight),
|
|
);
|
|
case FlightCommand.cancelLanding:
|
|
case FlightCommand.cancelGoHome:
|
|
case FlightCommand.stopMode:
|
|
// Only reached when the gate refused: show why.
|
|
return SlideConfirmSpec(
|
|
icon: 'stop',
|
|
title: labelOf(c),
|
|
description: 'This cannot be done right now.',
|
|
sliderLabel: labelOf(c),
|
|
color: Glass.accent,
|
|
checks: v.checks,
|
|
);
|
|
}
|
|
}
|
|
}
|