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, tapFly, 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 _waypointModes = {'GPS_WAYPOINT'}; static const Set _trackModes = {'ACTIVE_TRACK', 'ACTIVE_TRACK_SPOTLIGHT'}; static const Set _otherModes = {'GPS_HOT_POINT', 'GPS_FOLLOW_ME', '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; case 'tapfly': return ActiveMode.tapFly; } final String mode = a.flightModeEnum ?? ''; if (_waypointModes.contains(mode)) return ActiveMode.waypoint; if (_trackModes.contains(mode)) return ActiveMode.activeTrack; if (mode == 'TAP_FLY') return ActiveMode.tapFly; 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)'; }