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>
93 lines
3.7 KiB
Dart
93 lines
3.7 KiB
Dart
import 'state/aircraft_state.dart';
|
|
import 'state/mission_state.dart';
|
|
|
|
/// Every command the flight screen can send that moves the aircraft (or stops
|
|
/// it moving). All of them go through `SafetyGate`.
|
|
enum FlightCommand { takeOff, land, cancelLanding, confirmLanding, goHome, cancelGoHome, stopMode }
|
|
|
|
/// What is running when the rail offers [FlightCommand.stopMode], i.e. which
|
|
/// operator "Stop" has to address.
|
|
enum ActiveMode { none, waypoint, activeTrack, other }
|
|
|
|
/// The left rail's slots for the current flight state (plan P3, GO 4's
|
|
/// `fpv_leftmenu`): one primary button that walks Take-off → Land → Cancel
|
|
/// landing / Confirm landing, an RTH slot that turns into Cancel RTH, and a
|
|
/// Stop button while an intelligent mode flies the aircraft.
|
|
///
|
|
/// Pure: read off the flight controller and mission state, so the whole state
|
|
/// machine is unit-tested without a widget.
|
|
class RailState {
|
|
const RailState({required this.primary, this.rth, this.mode = ActiveMode.none, this.enabled = true});
|
|
|
|
/// Take-off, Land, Cancel landing or Confirm landing.
|
|
final FlightCommand primary;
|
|
|
|
/// Go home or Cancel RTH; null on the ground.
|
|
final FlightCommand? rth;
|
|
|
|
/// The intelligent mode a Stop button would end; [ActiveMode.none] hides it.
|
|
final ActiveMode mode;
|
|
|
|
/// False until the flight controller has reported: the buttons show but do nothing.
|
|
final bool enabled;
|
|
|
|
bool get showStop => mode != ActiveMode.none;
|
|
|
|
/// Flight-controller modes in which an intelligent mode is flying the aircraft.
|
|
static const Set<String> _waypointModes = <String>{'GPS_WAYPOINT'};
|
|
static const Set<String> _trackModes = <String>{'ACTIVE_TRACK', 'ACTIVE_TRACK_SPOTLIGHT'};
|
|
static const Set<String> _otherModes = <String>{'GPS_HOT_POINT', 'GPS_FOLLOW_ME', 'TAP_FLY', 'DRAW', 'TERRAIN_FOLLOW'};
|
|
|
|
static RailState of({required bool connected, required AircraftState a, MissionState? mission}) {
|
|
final bool reported = connected && a.updatedAt != null;
|
|
final bool airborne = a.isFlying == true;
|
|
final String mode = a.flightModeEnum ?? '';
|
|
final bool goingHome = a.goingHome == true || mode == 'GO_HOME';
|
|
|
|
final FlightCommand primary;
|
|
if (a.landingConfirmationNeeded == true || mode == 'CONFIRM_LANDING') {
|
|
primary = FlightCommand.confirmLanding;
|
|
} else if (mode == 'AUTO_LANDING') {
|
|
primary = FlightCommand.cancelLanding;
|
|
} else if (airborne) {
|
|
primary = FlightCommand.land;
|
|
} else {
|
|
primary = FlightCommand.takeOff;
|
|
}
|
|
|
|
return RailState(
|
|
primary: primary,
|
|
rth: airborne ? (goingHome ? FlightCommand.cancelGoHome : FlightCommand.goHome) : null,
|
|
mode: airborne ? activeMode(a, mission) : ActiveMode.none,
|
|
enabled: reported,
|
|
);
|
|
}
|
|
|
|
/// The intelligent mode in control, from the bridge's mission state first
|
|
/// (it knows which operator it started) and the flight mode second.
|
|
static ActiveMode activeMode(AircraftState a, MissionState? mission) {
|
|
switch (mission?.state) {
|
|
case 'executing':
|
|
return ActiveMode.waypoint;
|
|
case 'tracking':
|
|
case 'quickshot':
|
|
return ActiveMode.activeTrack;
|
|
}
|
|
final String mode = a.flightModeEnum ?? '';
|
|
if (_waypointModes.contains(mode)) return ActiveMode.waypoint;
|
|
if (_trackModes.contains(mode)) return ActiveMode.activeTrack;
|
|
if (_otherModes.contains(mode)) return ActiveMode.other;
|
|
return ActiveMode.none;
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
other is RailState && other.primary == primary && other.rth == rth && other.mode == mode && other.enabled == enabled;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(primary, rth, mode, enabled);
|
|
|
|
@override
|
|
String toString() => 'RailState($primary, rth: $rth, mode: $mode, enabled: $enabled)';
|
|
}
|